Passed
Push — 1.11.x ( 397ff2...44ef98 )
by Angel Fernando Quiroz
09:24
created

PortfolioController::details()   F

Complexity

Conditions 38
Paths 512

Size

Total Lines 361
Code Lines 248

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 248
c 0
b 0
f 0
dl 0
loc 361
rs 0.5422
cc 38
nc 512
nop 1

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

575
                    $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...
576
577
                    $teachers = SessionManager::getCoachesByCourseSession(
578
                        $this->session->getId(),
579
                        $this->course->getId()
580
                    );
581
                    $userIdListToSend = array_values($teachers);
582
                } else {
583
                    $messageCourseTitle = $this->course->getTitle();
584
585
                    $teachers = CourseManager::get_teacher_list_from_course_code($this->course->getCode());
586
587
                    $userIdListToSend = array_keys($teachers);
588
                }
589
590
                $messageSubject = sprintf(get_lang('PortfolioAlertNewPostSubject'), $messageCourseTitle);
591
                $messageContent = sprintf(
592
                    get_lang('PortfolioAlertNewPostContent'),
593
                    $this->owner->getCompleteName(),
594
                    $messageCourseTitle,
595
                    $this->baseUrl.http_build_query(['action' => 'view', 'id' => $portfolio->getId()])
596
                );
597
                $messageContent .= '<br><br><dl>'
598
                    .'<dt>'.Security::remove_XSS($portfolio->getTitle()).'</dt>'
599
                    .'<dd>'.$portfolio->getExcerpt().'</dd>'.'</dl>';
600
601
                foreach ($userIdListToSend as $userIdToSend) {
602
                    MessageManager::send_message_simple(
603
                        $userIdToSend,
604
                        $messageSubject,
605
                        $messageContent,
606
                        0,
607
                        false,
608
                        false,
609
                        [],
610
                        false
611
                    );
612
                }
613
            }
614
615
            Display::addFlash(
616
                Display::return_message(get_lang('PortfolioItemAdded'), 'success')
617
            );
618
619
            header("Location: $this->baseUrl");
620
            exit;
621
        }
622
623
        $interbreadcrumb[] = [
624
            'name' => get_lang('Portfolio'),
625
            'url' => $this->baseUrl,
626
        ];
627
628
        $actions = [];
629
        $actions[] = Display::url(
630
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
631
            $this->baseUrl
632
        );
633
        $actions[] = '<a id="hide_bar_template" href="#" role="button">'.
634
            Display::return_icon('expand.png', get_lang('Expand'), ['id' => 'expand'], ICON_SIZE_MEDIUM).
635
            Display::return_icon('contract.png', get_lang('Collapse'), ['id' => 'contract', 'class' => 'hide'], ICON_SIZE_MEDIUM).'</a>';
636
637
        $js = '<script>
638
            $(function() {
639
                $(".scrollbar-light").scrollbar();
640
                $(".scroll-wrapper").css("height", "550px");
641
                expandColumnToogle("#hide_bar_template", {
642
                    selector: "#template_col",
643
                    width: 3
644
                }, {
645
                    selector: "#doc_form",
646
                    width: 9
647
                });
648
                CKEDITOR.on("instanceReady", function (e) {
649
                    showTemplates();
650
                });
651
                $(window).on("load", function () {
652
                    $("input[name=\'title\']").focus();
653
                });
654
                $(\'#add_portfolio_template\').on(\'change\', function () {
655
                    $(\'#portfolio-spinner\').show();
656
                
657
                    $.getJSON(_p.web_ajax + \'portfolio.ajax.php?a=find_template&item=\' + this.value)
658
                        .done(function(response) {
659
                            if (CKEDITOR.instances.title) {
660
                                CKEDITOR.instances.title.setData(response.title);
661
                            } else {
662
                                document.getElementById(\'add_portfolio_title\').value = response.title;
663
                            }
664
665
                            CKEDITOR.instances.content.setData(response.content);
666
                        })
667
                        .fail(function () {
668
                            if (CKEDITOR.instances.title) {
669
                                CKEDITOR.instances.title.setData(\'\');
670
                            } else {
671
                                document.getElementById(\'add_portfolio_title\').value = \'\';
672
                            }
673
674
                            CKEDITOR.instances.content.setData(\'\');
675
                        })
676
                        .always(function() {
677
                          $(\'#portfolio-spinner\').hide();
678
                        });
679
                });
680
                '.$extra['jquery_ready_content'].'
681
            });
682
        </script>';
683
        $content = '<div class="page-create">
684
            <div class="row" style="overflow:hidden">
685
            <div id="template_col" class="col-md-3">
686
                <div class="panel panel-default">
687
                <div class="panel-body">
688
                    <div id="frmModel" class="items-templates scrollbar-light"></div>
689
                </div>
690
                </div>
691
            </div>
692
            <div id="doc_form" class="col-md-9">
693
                '.$form->returnForm().'
694
            </div>
695
          </div></div>';
696
697
        $this->renderView(
698
            $content.$js,
699
            get_lang('AddPortfolioItem'),
700
            $actions
701
        );
702
    }
703
704
    /**
705
     * @throws \Doctrine\ORM\ORMException
706
     * @throws \Doctrine\ORM\OptimisticLockException
707
     * @throws \Doctrine\ORM\TransactionRequiredException
708
     * @throws \Exception
709
     */
710
    public function editItem(Portfolio $item)
711
    {
712
        global $interbreadcrumb;
713
714
        if (!api_is_allowed_to_edit() && !$this->itemBelongToOwner($item)) {
715
            api_not_allowed(true);
716
        }
717
718
        $itemCourse = $item->getCourse();
719
        $itemSession = $item->getSession();
720
721
        $form = new FormValidator('edit_portfolio', 'post', $this->baseUrl."action=edit_item&id={$item->getId()}");
722
723
        if (api_get_configuration_value('save_titles_as_html')) {
724
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
725
        } else {
726
            $form->addText('title', get_lang('Title'));
727
            $form->applyFilter('title', 'trim');
728
        }
729
730
        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...
731
            if (Portfolio::TYPE_ITEM === $item->getOriginType()) {
732
                $origin = $this->em->find(Portfolio::class, $item->getOrigin());
733
734
                $form->addLabel(
735
                    sprintf(get_lang('PortfolioItemFromXUser'), $origin->getUser()->getCompleteName()),
736
                    Display::panel(
737
                        Security::remove_XSS($origin->getContent())
738
                    )
739
                );
740
            } elseif (Portfolio::TYPE_COMMENT === $item->getOriginType()) {
741
                $origin = $this->em->find(PortfolioComment::class, $item->getOrigin());
742
743
                $form->addLabel(
744
                    sprintf(get_lang('PortfolioCommentFromXUser'), $origin->getAuthor()->getCompleteName()),
745
                    Display::panel(
746
                        Security::remove_XSS($origin->getContent())
747
                    )
748
                );
749
            }
750
        }
751
        $editorConfig = [
752
            'ToolbarSet' => 'NotebookStudent',
753
            'Width' => '100%',
754
            'Height' => '400',
755
            'cols-size' => [2, 10, 0],
756
        ];
757
        $form->addHtmlEditor('content', get_lang('Content'), true, false, $editorConfig);
758
        $categoriesSelect = $form->addSelect(
759
            'category',
760
            [get_lang('Category'), get_lang('PortfolioCategoryFieldHelp')]
761
        );
762
        $categoriesSelect->addOption(get_lang('SelectACategory'), 0);
763
        $parentCategories = $this->getCategoriesForIndex(null, 0);
764
        foreach ($parentCategories as $parentCategory) {
765
            $categoriesSelect->addOption($this->translateDisplayName($parentCategory->getTitle()), $parentCategory->getId());
766
            $subCategories = $this->getCategoriesForIndex(null, $parentCategory->getId());
767
            if (count($subCategories) > 0) {
768
                foreach ($subCategories as $subCategory) {
769
                    $categoriesSelect->addOption(' &mdash; '.$this->translateDisplayName($subCategory->getTitle()), $subCategory->getId());
770
                }
771
            }
772
        }
773
774
        $extraField = new ExtraField('portfolio');
775
        $extra = $extraField->addElements(
776
            $form,
777
            $item->getId(),
778
            $this->course ? [] : ['tags']
779
        );
780
781
        $attachmentList = $this->generateAttachmentList($item, false);
782
783
        if (!empty($attachmentList)) {
784
            $form->addLabel(get_lang('AttachmentFiles'), $attachmentList);
785
        }
786
787
        $this->addAttachmentsFieldToForm($form);
788
789
        $form->addButtonUpdate(get_lang('Update'));
790
        $form->setDefaults(
791
            [
792
                'title' => $item->getTitle(),
793
                'content' => $item->getContent(),
794
                'category' => $item->getCategory() ? $item->getCategory()->getId() : '',
795
            ]
796
        );
797
798
        if ($form->validate()) {
799
            if ($itemCourse) {
0 ignored issues
show
introduced by
$itemCourse is of type Chamilo\CoreBundle\Entity\Course, thus it always evaluated to true.
Loading history...
800
                api_item_property_update(
801
                    api_get_course_info($itemCourse->getCode()),
802
                    TOOL_PORTFOLIO,
803
                    $item->getId(),
804
                    'PortfolioUpdated',
805
                    api_get_user_id(),
806
                    [],
807
                    null,
808
                    '',
809
                    '',
810
                    $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...
811
                );
812
            }
813
814
            $values = $form->exportValues();
815
            $currentTime = new DateTime(api_get_utc_datetime(), new DateTimeZone('UTC'));
816
817
            $item
818
                ->setTitle($values['title'])
819
                ->setContent($values['content'])
820
                ->setUpdateDate($currentTime)
821
                ->setCategory(
822
                    $this->em->find('ChamiloCoreBundle:PortfolioCategory', $values['category'])
823
                );
824
825
            $values['item_id'] = $item->getId();
826
827
            $extraFieldValue = new ExtraFieldValue('portfolio');
828
            $extraFieldValue->saveFieldValues($values);
829
830
            $this->em->persist($item);
831
            $this->em->flush();
832
833
            $this->processAttachments(
834
                $form,
835
                $item->getUser(),
836
                $item->getId(),
837
                PortfolioAttachment::TYPE_ITEM
838
            );
839
840
            Display::addFlash(
841
                Display::return_message(get_lang('ItemUpdated'), 'success')
842
            );
843
844
            header("Location: $this->baseUrl");
845
            exit;
846
        }
847
848
        $interbreadcrumb[] = [
849
            'name' => get_lang('Portfolio'),
850
            'url' => $this->baseUrl,
851
        ];
852
        $actions = [];
853
        $actions[] = Display::url(
854
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
855
            $this->baseUrl
856
        );
857
        $actions[] = '<a id="hide_bar_template" href="#" role="button">'.
858
            Display::return_icon('expand.png', get_lang('Expand'), ['id' => 'expand'], ICON_SIZE_MEDIUM).
859
            Display::return_icon('contract.png', get_lang('Collapse'), ['id' => 'contract', 'class' => 'hide'], ICON_SIZE_MEDIUM).'</a>';
860
861
        $js = '<script>
862
            $(function() {
863
                $(".scrollbar-light").scrollbar();
864
                $(".scroll-wrapper").css("height", "550px");
865
                expandColumnToogle("#hide_bar_template", {
866
                    selector: "#template_col",
867
                    width: 3
868
                }, {
869
                    selector: "#doc_form",
870
                    width: 9
871
                });
872
                CKEDITOR.on("instanceReady", function (e) {
873
                    showTemplates();
874
                });
875
                $(window).on("load", function () {
876
                    $("input[name=\'title\']").focus();
877
                });
878
                '.$extra['jquery_ready_content'].'
879
            });
880
        </script>';
881
        $content = '<div class="page-create">
882
            <div class="row" style="overflow:hidden">
883
            <div id="template_col" class="col-md-3">
884
                <div class="panel panel-default">
885
                <div class="panel-body">
886
                    <div id="frmModel" class="items-templates scrollbar-light"></div>
887
                </div>
888
                </div>
889
            </div>
890
            <div id="doc_form" class="col-md-9">
891
                '.$form->returnForm().'
892
            </div>
893
          </div></div>';
894
895
        $this->renderView(
896
            $content.$js,
897
            get_lang('EditPortfolioItem'),
898
            $actions
899
        );
900
    }
901
902
    /**
903
     * @throws \Doctrine\ORM\ORMException
904
     * @throws \Doctrine\ORM\OptimisticLockException
905
     */
906
    public function showHideItem(Portfolio $item)
907
    {
908
        if (!$this->itemBelongToOwner($item)) {
909
            api_not_allowed(true);
910
        }
911
912
        switch ($item->getVisibility()) {
913
            case Portfolio::VISIBILITY_HIDDEN:
914
                $item->setVisibility(Portfolio::VISIBILITY_VISIBLE);
915
                break;
916
            case Portfolio::VISIBILITY_VISIBLE:
917
                $item->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER);
918
                break;
919
            case Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER:
920
            default:
921
                $item->setVisibility(Portfolio::VISIBILITY_HIDDEN);
922
                break;
923
        }
924
925
        $this->em->persist($item);
926
        $this->em->flush();
927
928
        Display::addFlash(
929
            Display::return_message(get_lang('VisibilityChanged'), 'success')
930
        );
931
932
        header("Location: $this->baseUrl");
933
        exit;
934
    }
935
936
    /**
937
     * @throws \Doctrine\ORM\ORMException
938
     * @throws \Doctrine\ORM\OptimisticLockException
939
     */
940
    public function deleteItem(Portfolio $item)
941
    {
942
        if (!$this->itemBelongToOwner($item)) {
943
            api_not_allowed(true);
944
        }
945
946
        $this->em->remove($item);
947
        $this->em->flush();
948
949
        Display::addFlash(
950
            Display::return_message(get_lang('ItemDeleted'), 'success')
951
        );
952
953
        header("Location: $this->baseUrl");
954
        exit;
955
    }
956
957
    /**
958
     * @throws \Exception
959
     */
960
    public function index(HttpRequest $httpRequest)
961
    {
962
        $listByUser = false;
963
        $listHighlighted = $httpRequest->query->has('list_highlighted');
964
965
        if ($httpRequest->query->has('user')) {
966
            $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
967
968
            if (empty($this->owner)) {
969
                api_not_allowed(true);
970
            }
971
972
            $listByUser = true;
973
        }
974
975
        $currentUserId = api_get_user_id();
976
977
        $actions = [];
978
979
        if (api_is_platform_admin()) {
980
            $actions[] = Display::url(
981
                Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
982
                $this->baseUrl.'action=add_item'
983
            );
984
            $actions[] = Display::url(
985
                Display::return_icon('folder.png', get_lang('AddCategory'), [], ICON_SIZE_MEDIUM),
986
                $this->baseUrl.'action=list_categories'
987
            );
988
            $actions[] = Display::url(
989
                Display::return_icon('waiting_list.png', get_lang('PortfolioDetails'), [], ICON_SIZE_MEDIUM),
990
                $this->baseUrl.'action=details'
991
            );
992
        } else {
993
            if ($currentUserId == $this->owner->getId()) {
994
                if ($this->isAllowed()) {
995
                    $actions[] = Display::url(
996
                        Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
997
                        $this->baseUrl.'action=add_item'
998
                    );
999
                    $actions[] = Display::url(
1000
                        Display::return_icon('waiting_list.png', get_lang('PortfolioDetails'), [], ICON_SIZE_MEDIUM),
1001
                        $this->baseUrl.'action=details'
1002
                    );
1003
                }
1004
            } else {
1005
                $actions[] = Display::url(
1006
                    Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1007
                    $this->baseUrl
1008
                );
1009
            }
1010
        }
1011
1012
        if (api_is_allowed_to_edit()) {
1013
            $actions[] = Display::url(
1014
                Display::return_icon('tickets.png', get_lang('Tags'), [], ICON_SIZE_MEDIUM),
1015
                $this->baseUrl.'action=tags'
1016
            );
1017
        }
1018
1019
        $frmStudentList = null;
1020
        $frmTagList = null;
1021
1022
        $categories = [];
1023
        $portfolio = [];
1024
        if ($this->course) {
1025
            $frmTagList = $this->createFormTagFilter($listByUser);
1026
            $frmStudentList = $this->createFormStudentFilter($listByUser, $listHighlighted);
1027
            $frmStudentList->setDefaults(['user' => $this->owner->getId()]);
1028
            // it translates the category title with the current user language
1029
            $categories = $this->getCategoriesForIndex(null, 0);
1030
            if (count($categories) > 0) {
1031
                foreach ($categories as &$category) {
1032
                    $translated = $this->translateDisplayName($category->getTitle());
1033
                    $category->setTitle($translated);
1034
                }
1035
            }
1036
        } else {
1037
            // it displays the list in Network Social for the current user
1038
            $portfolio = $this->getCategoriesForIndex();
1039
        }
1040
1041
        $foundComments = [];
1042
1043
        if ($listHighlighted) {
1044
            $items = $this->getHighlightedItems();
1045
        } else {
1046
            $items = $this->getItemsForIndex($listByUser, $frmTagList);
1047
1048
            $foundComments = $this->getCommentsForIndex($frmTagList);
1049
        }
1050
1051
        // it gets and translate the sub-categories
1052
        $categoryId = $httpRequest->query->getInt('categoryId');
1053
        $subCategoryIdsReq = isset($_REQUEST['subCategoryIds']) ? Security::remove_XSS($_REQUEST['subCategoryIds']) : '';
1054
        $subCategoryIds = $subCategoryIdsReq;
1055
        if ('all' !== $subCategoryIdsReq) {
1056
            $subCategoryIds = !empty($subCategoryIdsReq) ? explode(',', $subCategoryIdsReq) : [];
1057
        }
1058
        $subcategories = [];
1059
        if ($categoryId > 0) {
1060
            $subcategories = $this->getCategoriesForIndex(null, $categoryId);
1061
            if (count($subcategories) > 0) {
1062
                foreach ($subcategories as &$subcategory) {
1063
                    $translated = $this->translateDisplayName($subcategory->getTitle());
1064
                    $subcategory->setTitle($translated);
1065
                }
1066
            }
1067
        }
1068
1069
        $template = new Template(null, false, false, false, false, false, false);
1070
        $template->assign('user', $this->owner);
1071
        $template->assign('course', $this->course);
1072
        $template->assign('session', $this->session);
1073
        $template->assign('portfolio', $portfolio);
1074
        $template->assign('categories', $categories);
1075
        $template->assign('uncategorized_items', $items);
1076
        $template->assign('frm_student_list', $this->course ? $frmStudentList->returnForm() : '');
1077
        $template->assign('frm_tag_list', $this->course ? $frmTagList->returnForm() : '');
1078
        $template->assign('category_id', $categoryId);
1079
        $template->assign('subcategories', $subcategories);
1080
        $template->assign('subcategory_ids', $subCategoryIds);
1081
        $template->assign('found_comments', $foundComments);
1082
1083
        $js = '<script>
1084
            $(function() {
1085
                $(".category-filters").bind("click", function() {
1086
                    var categoryId = parseInt($(this).find("input[type=\'radio\']").val());
1087
                    $("input[name=\'categoryId\']").val(categoryId);
1088
                    $("input[name=\'subCategoryIds\']").val("all");
1089
                    $("#frm_tag_list_submit").trigger("click");
1090
                });
1091
                $(".subcategory-filters").bind("click", function() {
1092
                    var checkedVals = $(".subcategory-filters:checkbox:checked").map(function() {
1093
                        return this.value;
1094
                    }).get();
1095
                    $("input[name=\'subCategoryIds\']").val(checkedVals.join(","));
1096
                    $("#frm_tag_list_submit").trigger("click");
1097
                });
1098
            });
1099
        </script>';
1100
        $template->assign('js_script', $js);
1101
        $layout = $template->get_template('portfolio/list.html.twig');
1102
1103
        Display::addFlash(
1104
            Display::return_message(get_lang('PortfolioPostAddHelp'), 'info', false)
1105
        );
1106
1107
        $content = $template->fetch($layout);
1108
1109
        $this->renderView($content, get_lang('Portfolio'), $actions);
1110
    }
1111
1112
    /**
1113
     * @throws \Doctrine\ORM\ORMException
1114
     * @throws \Doctrine\ORM\OptimisticLockException
1115
     * @throws \Doctrine\ORM\TransactionRequiredException
1116
     */
1117
    public function view(Portfolio $item)
1118
    {
1119
        global $interbreadcrumb;
1120
1121
        if (!$this->itemBelongToOwner($item)) {
1122
            if ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN
1123
                || ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER && !api_is_allowed_to_edit())
1124
            ) {
1125
                api_not_allowed(true);
1126
            }
1127
        }
1128
1129
        HookPortfolioItemViewed::create()
1130
            ->setEventData(['portfolio' => $item])
1131
            ->notifyItemViewed()
1132
        ;
1133
1134
        $itemCourse = $item->getCourse();
1135
        $itemSession = $item->getSession();
1136
1137
        $form = $this->createCommentForm($item);
1138
1139
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
1140
1141
        $query = $commentsRepo->createQueryBuilder('comment')
1142
            ->where('comment.item = :item')
1143
            ->orderBy('comment.root, comment.lft', 'ASC')
1144
            ->setParameter('item', $item)
1145
            ->getQuery();
1146
1147
        $clockIcon = Display::returnFontAwesomeIcon('clock-o', '', true);
1148
1149
        $commentsHtml = $commentsRepo->buildTree(
1150
            $query->getArrayResult(),
1151
            [
1152
                'decorate' => true,
1153
                'rootOpen' => '<div class="media-list">',
1154
                'rootClose' => '</div>',
1155
                'childOpen' => function ($node) use ($commentsRepo) {
1156
                    /** @var PortfolioComment $comment */
1157
                    $comment = $commentsRepo->find($node['id']);
1158
                    $author = $comment->getAuthor();
1159
1160
                    $userPicture = UserManager::getUserPicture(
1161
                        $comment->getAuthor()->getId(),
1162
                        USER_IMAGE_SIZE_SMALL,
1163
                        null,
1164
                        [
1165
                            'picture_uri' => $author->getPictureUri(),
1166
                            'email' => $author->getEmail(),
1167
                        ]
1168
                    );
1169
1170
                    return '<article class="media" id="comment-'.$node['id'].'">
1171
                        <div class="media-left"><img class="media-object thumbnail" src="'.$userPicture.'" alt="'
1172
                        .$author->getCompleteName().'"></div>
1173
                        <div class="media-body">';
1174
                },
1175
                'childClose' => '</div></article>',
1176
                'nodeDecorator' => function ($node) use ($commentsRepo, $clockIcon, $item) {
1177
                    $commentActions = [];
1178
                    /** @var PortfolioComment $comment */
1179
                    $comment = $commentsRepo->find($node['id']);
1180
1181
                    if ($this->commentBelongsToOwner($comment)) {
1182
                        $commentActions[] = Display::url(
1183
                            Display::return_icon(
1184
                                $comment->isTemplate() ? 'wizard.png' : 'wizard_na.png',
1185
                                $comment->isTemplate() ? get_lang('RemoveAsTemplate') : get_lang('AddAsTemplate')
1186
                            ),
1187
                            $this->baseUrl.http_build_query(['action' => 'template_comment', 'id' => $comment->getId()])
1188
                        );
1189
                    }
1190
1191
                    $commentActions[] = Display::url(
1192
                        Display::return_icon('discuss.png', get_lang('ReplyToThisComment')),
1193
                        '#',
1194
                        [
1195
                            'data-comment' => htmlspecialchars(
1196
                                json_encode(['id' => $comment->getId()])
1197
                            ),
1198
                            'role' => 'button',
1199
                            'class' => 'btn-reply-to',
1200
                        ]
1201
                    );
1202
                    $commentActions[] = Display::url(
1203
                        Display::return_icon('copy.png', get_lang('CopyToMyPortfolio')),
1204
                        $this->baseUrl.http_build_query(
1205
                            [
1206
                                'action' => 'copy',
1207
                                'copy' => 'comment',
1208
                                'id' => $comment->getId(),
1209
                            ]
1210
                        )
1211
                    );
1212
1213
                    $isAllowedToEdit = api_is_allowed_to_edit();
1214
1215
                    if ($isAllowedToEdit) {
1216
                        $commentActions[] = Display::url(
1217
                            Display::return_icon('copy.png', get_lang('CopyToStudentPortfolio')),
1218
                            $this->baseUrl.http_build_query(
1219
                                [
1220
                                    'action' => 'teacher_copy',
1221
                                    'copy' => 'comment',
1222
                                    'id' => $comment->getId(),
1223
                                ]
1224
                            )
1225
                        );
1226
1227
                        if ($comment->isImportant()) {
1228
                            $commentActions[] = Display::url(
1229
                                Display::return_icon('drawing-pin.png', get_lang('UnmarkCommentAsImportant')),
1230
                                $this->baseUrl.http_build_query(
1231
                                    [
1232
                                        'action' => 'mark_important',
1233
                                        'item' => $item->getId(),
1234
                                        'id' => $comment->getId(),
1235
                                    ]
1236
                                )
1237
                            );
1238
                        } else {
1239
                            $commentActions[] = Display::url(
1240
                                Display::return_icon('drawing-pin.png', get_lang('MarkCommentAsImportant')),
1241
                                $this->baseUrl.http_build_query(
1242
                                    [
1243
                                        'action' => 'mark_important',
1244
                                        'item' => $item->getId(),
1245
                                        'id' => $comment->getId(),
1246
                                    ]
1247
                                )
1248
                            );
1249
                        }
1250
1251
                        if ($this->course && '1' === api_get_course_setting('qualify_portfolio_comment')) {
1252
                            $commentActions[] = Display::url(
1253
                                Display::return_icon('quiz.png', get_lang('QualifyThisPortfolioComment')),
1254
                                $this->baseUrl.http_build_query(
1255
                                    [
1256
                                        'action' => 'qualify',
1257
                                        'comment' => $comment->getId(),
1258
                                    ]
1259
                                )
1260
                            );
1261
                        }
1262
                    }
1263
1264
                    if ($this->commentBelongsToOwner($comment)) {
1265
                        $commentActions[] = Display::url(
1266
                            Display::return_icon('edit.png', get_lang('Edit')),
1267
                            $this->baseUrl.http_build_query(['action' => 'edit_comment', 'id' => $comment->getId()])
1268
                        );
1269
                        $commentActions[] = Display::url(
1270
                            Display::return_icon('delete.png', get_lang('Delete')),
1271
                            $this->baseUrl.http_build_query(['action' => 'delete_comment', 'id' => $comment->getId()])
1272
                        );
1273
                    }
1274
1275
                    $nodeHtml = '<div class="pull-right">'.implode(PHP_EOL, $commentActions).'</div>'.PHP_EOL
1276
                        .'<footer class="media-heading h4">'.PHP_EOL
1277
                        .'<p>'.$comment->getAuthor()->getCompleteName().'</p>'.PHP_EOL;
1278
1279
                    if ($comment->isImportant()
1280
                        && ($this->itemBelongToOwner($comment->getItem()) || $isAllowedToEdit)
1281
                    ) {
1282
                        $nodeHtml .= '<span class="pull-right label label-warning origin-style">'
1283
                            .get_lang('CommentMarkedAsImportant')
1284
                            .'</span>'.PHP_EOL;
1285
                    }
1286
1287
                    $nodeHtml .= '<small>'.$clockIcon.PHP_EOL
1288
                        .$this->getLabelForCommentDate($comment).'</small>'.PHP_EOL;
1289
1290
                    $nodeHtml .= '</footer>'.PHP_EOL
1291
                        .Security::remove_XSS($comment->getContent()).PHP_EOL;
1292
1293
                    $nodeHtml .= $this->generateAttachmentList($comment);
1294
1295
                    return $nodeHtml;
1296
                },
1297
            ]
1298
        );
1299
1300
        $template = new Template(null, false, false, false, false, false, false);
1301
        $template->assign('baseurl', $this->baseUrl);
1302
        $template->assign('item', $item);
1303
        $template->assign('item_content', $this->generateItemContent($item));
1304
        $template->assign('comments', $commentsHtml);
1305
        $template->assign('form', $form);
1306
        $template->assign('attachment_list', $this->generateAttachmentList($item));
1307
1308
        if ($itemCourse) {
1309
            $propertyInfo = api_get_item_property_info(
1310
                $itemCourse->getId(),
1311
                TOOL_PORTFOLIO,
1312
                $item->getId(),
1313
                $itemSession ? $itemSession->getId() : 0
1314
            );
1315
1316
            if ($propertyInfo) {
1317
                $template->assign(
1318
                    'last_edit',
1319
                    [
1320
                        'date' => $propertyInfo['lastedit_date'],
1321
                        'user' => api_get_user_entity($propertyInfo['lastedit_user_id'])->getCompleteName(),
1322
                    ]
1323
                );
1324
            }
1325
        }
1326
1327
        $layout = $template->get_template('portfolio/view.html.twig');
1328
        $content = $template->fetch($layout);
1329
1330
        $interbreadcrumb[] = ['name' => get_lang('Portfolio'), 'url' => $this->baseUrl];
1331
1332
        $editLink = Display::url(
1333
            Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_MEDIUM),
1334
            $this->baseUrl.http_build_query(['action' => 'edit_item', 'id' => $item->getId()])
1335
        );
1336
1337
        $actions = [];
1338
        $actions[] = Display::url(
1339
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1340
            $this->baseUrl
1341
        );
1342
1343
        if ($this->itemBelongToOwner($item)) {
1344
            $actions[] = $editLink;
1345
1346
            $actions[] = Display::url(
1347
                Display::return_icon(
1348
                    $item->isTemplate() ? 'wizard.png' : 'wizard_na.png',
1349
                    $item->isTemplate() ? get_lang('RemoveAsTemplate') : get_lang('AddAsTemplate'),
1350
                    [],
1351
                    ICON_SIZE_MEDIUM
1352
                ),
1353
                $this->baseUrl.http_build_query(['action' => 'template', 'id' => $item->getId()])
1354
            );
1355
1356
            $visibilityUrl = $this->baseUrl.http_build_query(['action' => 'visibility', 'id' => $item->getId()]);
1357
1358
            if ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN) {
1359
                $actions[] = Display::url(
1360
                    Display::return_icon('invisible.png', get_lang('MakeVisible'), [], ICON_SIZE_MEDIUM),
1361
                    $visibilityUrl
1362
                );
1363
            } elseif ($item->getVisibility() === Portfolio::VISIBILITY_VISIBLE) {
1364
                $actions[] = Display::url(
1365
                    Display::return_icon('visible.png', get_lang('MakeVisibleForTeachers'), [], ICON_SIZE_MEDIUM),
1366
                    $visibilityUrl
1367
                );
1368
            } elseif ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER) {
1369
                $actions[] = Display::url(
1370
                    Display::return_icon('eye-slash.png', get_lang('MakeInvisible'), [], ICON_SIZE_MEDIUM),
1371
                    $visibilityUrl
1372
                );
1373
            }
1374
1375
            $actions[] = Display::url(
1376
                Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_MEDIUM),
1377
                $this->baseUrl.http_build_query(['action' => 'delete_item', 'id' => $item->getId()])
1378
            );
1379
        } else {
1380
            $actions[] = Display::url(
1381
                Display::return_icon('copy.png', get_lang('CopyToMyPortfolio'), [], ICON_SIZE_MEDIUM),
1382
                $this->baseUrl.http_build_query(['action' => 'copy', 'copy' => 'item', 'id' => $item->getId()])
1383
            );
1384
        }
1385
1386
        if (api_is_allowed_to_edit()) {
1387
            $actions[] = Display::url(
1388
                Display::return_icon('copy.png', get_lang('CopyToStudentPortfolio'), [], ICON_SIZE_MEDIUM),
1389
                $this->baseUrl.http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $item->getId()])
1390
            );
1391
            $actions[] = $editLink;
1392
1393
            $highlightedUrl = $this->baseUrl.http_build_query(['action' => 'highlighted', 'id' => $item->getId()]);
1394
1395
            if ($item->isHighlighted()) {
1396
                $actions[] = Display::url(
1397
                    Display::return_icon('award_red.png', get_lang('UnmarkAsHighlighted'), [], ICON_SIZE_MEDIUM),
1398
                    $highlightedUrl
1399
                );
1400
            } else {
1401
                $actions[] = Display::url(
1402
                    Display::return_icon('award_red_na.png', get_lang('MarkAsHighlighted'), [], ICON_SIZE_MEDIUM),
1403
                    $highlightedUrl
1404
                );
1405
            }
1406
1407
            if ($itemCourse && '1' === api_get_course_setting('qualify_portfolio_item')) {
1408
                $actions[] = Display::url(
1409
                    Display::return_icon('quiz.png', get_lang('QualifyThisPortfolioItem'), [], ICON_SIZE_MEDIUM),
1410
                    $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()])
1411
                );
1412
            }
1413
        }
1414
1415
        $this->renderView($content, $item->getTitle(true), $actions, false);
1416
    }
1417
1418
    /**
1419
     * @throws \Doctrine\ORM\ORMException
1420
     * @throws \Doctrine\ORM\OptimisticLockException
1421
     */
1422
    public function copyItem(Portfolio $originItem)
1423
    {
1424
        $this->blockIsNotAllowed();
1425
1426
        $currentTime = api_get_utc_datetime(null, false, true);
1427
1428
        $portfolio = new Portfolio();
1429
        $portfolio
1430
            ->setVisibility(Portfolio::VISIBILITY_HIDDEN)
1431
            ->setTitle(
1432
                sprintf(get_lang('PortfolioItemFromXUser'), $originItem->getUser()->getCompleteName())
1433
            )
1434
            ->setContent('')
1435
            ->setUser($this->owner)
1436
            ->setOrigin($originItem->getId())
1437
            ->setOriginType(Portfolio::TYPE_ITEM)
1438
            ->setCourse($this->course)
1439
            ->setSession($this->session)
1440
            ->setCreationDate($currentTime)
1441
            ->setUpdateDate($currentTime);
1442
1443
        $this->em->persist($portfolio);
1444
        $this->em->flush();
1445
1446
        Display::addFlash(
1447
            Display::return_message(get_lang('PortfolioItemAdded'), 'success')
1448
        );
1449
1450
        header("Location: $this->baseUrl".http_build_query(['action' => 'edit_item', 'id' => $portfolio->getId()]));
1451
        exit;
1452
    }
1453
1454
    /**
1455
     * @throws \Doctrine\ORM\ORMException
1456
     * @throws \Doctrine\ORM\OptimisticLockException
1457
     */
1458
    public function copyComment(PortfolioComment $originComment)
1459
    {
1460
        $currentTime = api_get_utc_datetime(null, false, true);
1461
1462
        $portfolio = new Portfolio();
1463
        $portfolio
1464
            ->setVisibility(Portfolio::VISIBILITY_HIDDEN)
1465
            ->setTitle(
1466
                sprintf(get_lang('PortfolioCommentFromXUser'), $originComment->getAuthor()->getCompleteName())
1467
            )
1468
            ->setContent('')
1469
            ->setUser($this->owner)
1470
            ->setOrigin($originComment->getId())
1471
            ->setOriginType(Portfolio::TYPE_COMMENT)
1472
            ->setCourse($this->course)
1473
            ->setSession($this->session)
1474
            ->setCreationDate($currentTime)
1475
            ->setUpdateDate($currentTime);
1476
1477
        $this->em->persist($portfolio);
1478
        $this->em->flush();
1479
1480
        Display::addFlash(
1481
            Display::return_message(get_lang('PortfolioItemAdded'), 'success')
1482
        );
1483
1484
        header("Location: $this->baseUrl".http_build_query(['action' => 'edit_item', 'id' => $portfolio->getId()]));
1485
        exit;
1486
    }
1487
1488
    /**
1489
     * @throws \Doctrine\ORM\ORMException
1490
     * @throws \Doctrine\ORM\OptimisticLockException
1491
     * @throws \Exception
1492
     */
1493
    public function teacherCopyItem(Portfolio $originItem)
1494
    {
1495
        api_protect_teacher_script();
1496
1497
        $actionParams = http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $originItem->getId()]);
1498
1499
        $form = new FormValidator('teacher_copy_portfolio', 'post', $this->baseUrl.$actionParams);
1500
1501
        if (api_get_configuration_value('save_titles_as_html')) {
1502
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
1503
        } else {
1504
            $form->addText('title', get_lang('Title'));
1505
            $form->applyFilter('title', 'trim');
1506
        }
1507
1508
        $form->addLabel(
1509
            sprintf(get_lang('PortfolioItemFromXUser'), $originItem->getUser()->getCompleteName()),
1510
            Display::panel(
1511
                Security::remove_XSS($originItem->getContent())
1512
            )
1513
        );
1514
        $form->addHtmlEditor('content', get_lang('Content'), true, false, ['ToolbarSet' => 'NotebookStudent']);
1515
1516
        $urlParams = http_build_query(
1517
            [
1518
                'a' => 'search_user_by_course',
1519
                'course_id' => $this->course->getId(),
1520
                'session_id' => $this->session ? $this->session->getId() : 0,
1521
            ]
1522
        );
1523
        $form->addSelectAjax(
1524
            'students',
1525
            get_lang('Students'),
1526
            [],
1527
            [
1528
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1529
                'multiple' => true,
1530
            ]
1531
        );
1532
        $form->addRule('students', get_lang('ThisFieldIsRequired'), 'required');
1533
        $form->addButtonCreate(get_lang('Save'));
1534
1535
        $toolName = get_lang('CopyToStudentPortfolio');
1536
        $content = $form->returnForm();
1537
1538
        if ($form->validate()) {
1539
            $values = $form->exportValues();
1540
1541
            $currentTime = api_get_utc_datetime(null, false, true);
1542
1543
            foreach ($values['students'] as $studentId) {
1544
                $owner = api_get_user_entity($studentId);
1545
1546
                $portfolio = new Portfolio();
1547
                $portfolio
1548
                    ->setVisibility(Portfolio::VISIBILITY_HIDDEN)
1549
                    ->setTitle($values['title'])
1550
                    ->setContent($values['content'])
1551
                    ->setUser($owner)
1552
                    ->setOrigin($originItem->getId())
1553
                    ->setOriginType(Portfolio::TYPE_ITEM)
1554
                    ->setCourse($this->course)
1555
                    ->setSession($this->session)
1556
                    ->setCreationDate($currentTime)
1557
                    ->setUpdateDate($currentTime);
1558
1559
                $this->em->persist($portfolio);
1560
            }
1561
1562
            $this->em->flush();
1563
1564
            Display::addFlash(
1565
                Display::return_message(get_lang('PortfolioItemAddedToStudents'), 'success')
1566
            );
1567
1568
            header("Location: $this->baseUrl");
1569
            exit;
1570
        }
1571
1572
        $this->renderView($content, $toolName);
1573
    }
1574
1575
    /**
1576
     * @throws \Doctrine\ORM\ORMException
1577
     * @throws \Doctrine\ORM\OptimisticLockException
1578
     * @throws \Exception
1579
     */
1580
    public function teacherCopyComment(PortfolioComment $originComment)
1581
    {
1582
        $actionParams = http_build_query(
1583
            [
1584
                'action' => 'teacher_copy',
1585
                'copy' => 'comment',
1586
                'id' => $originComment->getId(),
1587
            ]
1588
        );
1589
1590
        $form = new FormValidator('teacher_copy_portfolio', 'post', $this->baseUrl.$actionParams);
1591
1592
        if (api_get_configuration_value('save_titles_as_html')) {
1593
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
1594
        } else {
1595
            $form->addText('title', get_lang('Title'));
1596
            $form->applyFilter('title', 'trim');
1597
        }
1598
1599
        $form->addLabel(
1600
            sprintf(get_lang('PortfolioCommentFromXUser'), $originComment->getAuthor()->getCompleteName()),
1601
            Display::panel(
1602
                Security::remove_XSS($originComment->getContent())
1603
            )
1604
        );
1605
        $form->addHtmlEditor('content', get_lang('Content'), true, false, ['ToolbarSet' => 'NotebookStudent']);
1606
1607
        $urlParams = http_build_query(
1608
            [
1609
                'a' => 'search_user_by_course',
1610
                'course_id' => $this->course->getId(),
1611
                'session_id' => $this->session ? $this->session->getId() : 0,
1612
            ]
1613
        );
1614
        $form->addSelectAjax(
1615
            'students',
1616
            get_lang('Students'),
1617
            [],
1618
            [
1619
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1620
                'multiple' => true,
1621
            ]
1622
        );
1623
        $form->addRule('students', get_lang('ThisFieldIsRequired'), 'required');
1624
        $form->addButtonCreate(get_lang('Save'));
1625
1626
        $toolName = get_lang('CopyToStudentPortfolio');
1627
        $content = $form->returnForm();
1628
1629
        if ($form->validate()) {
1630
            $values = $form->exportValues();
1631
1632
            $currentTime = api_get_utc_datetime(null, false, true);
1633
1634
            foreach ($values['students'] as $studentId) {
1635
                $owner = api_get_user_entity($studentId);
1636
1637
                $portfolio = new Portfolio();
1638
                $portfolio
1639
                    ->setVisibility(Portfolio::VISIBILITY_HIDDEN)
1640
                    ->setTitle($values['title'])
1641
                    ->setContent($values['content'])
1642
                    ->setUser($owner)
1643
                    ->setOrigin($originComment->getId())
1644
                    ->setOriginType(Portfolio::TYPE_COMMENT)
1645
                    ->setCourse($this->course)
1646
                    ->setSession($this->session)
1647
                    ->setCreationDate($currentTime)
1648
                    ->setUpdateDate($currentTime);
1649
1650
                $this->em->persist($portfolio);
1651
            }
1652
1653
            $this->em->flush();
1654
1655
            Display::addFlash(
1656
                Display::return_message(get_lang('PortfolioItemAddedToStudents'), 'success')
1657
            );
1658
1659
            header("Location: $this->baseUrl");
1660
            exit;
1661
        }
1662
1663
        $this->renderView($content, $toolName);
1664
    }
1665
1666
    /**
1667
     * @throws \Doctrine\ORM\ORMException
1668
     * @throws \Doctrine\ORM\OptimisticLockException
1669
     */
1670
    public function markImportantCommentInItem(Portfolio $item, PortfolioComment $comment)
1671
    {
1672
        if ($comment->getItem()->getId() !== $item->getId()) {
1673
            api_not_allowed(true);
1674
        }
1675
1676
        $comment->setIsImportant(
1677
            !$comment->isImportant()
1678
        );
1679
1680
        $this->em->persist($comment);
1681
        $this->em->flush();
1682
1683
        Display::addFlash(
1684
            Display::return_message(get_lang('CommentMarkedAsImportant'), 'success')
1685
        );
1686
1687
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
1688
        exit;
1689
    }
1690
1691
    /**
1692
     * @throws \Exception
1693
     */
1694
    public function details(HttpRequest $httpRequest)
1695
    {
1696
        $this->blockIsNotAllowed();
1697
1698
        $currentUserId = api_get_user_id();
1699
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
1700
1701
        $actions = [];
1702
        $actions[] = Display::url(
1703
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1704
            $this->baseUrl
1705
        );
1706
        $actions[] = Display::url(
1707
            Display::return_icon('pdf.png', get_lang('ExportMyPortfolioDataPdf'), [], ICON_SIZE_MEDIUM),
1708
            $this->baseUrl.http_build_query(['action' => 'export_pdf'])
1709
        );
1710
        $actions[] = Display::url(
1711
            Display::return_icon('save_pack.png', get_lang('ExportMyPortfolioDataZip'), [], ICON_SIZE_MEDIUM),
1712
            $this->baseUrl.http_build_query(['action' => 'export_zip'])
1713
        );
1714
1715
        $frmStudent = null;
1716
1717
        if ($isAllowedToFilterStudent) {
1718
            if ($httpRequest->query->has('user')) {
1719
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
1720
1721
                if (empty($this->owner)) {
1722
                    api_not_allowed(true);
1723
                }
1724
1725
                $actions[1] = Display::url(
1726
                    Display::return_icon('pdf.png', get_lang('ExportMyPortfolioDataPdf'), [], ICON_SIZE_MEDIUM),
1727
                    $this->baseUrl.http_build_query(['action' => 'export_pdf', 'user' => $this->owner->getId()])
1728
                );
1729
                $actions[2] = Display::url(
1730
                    Display::return_icon('save_pack.png', get_lang('ExportMyPortfolioDataZip'), [], ICON_SIZE_MEDIUM),
1731
                    $this->baseUrl.http_build_query(['action' => 'export_zip', 'user' => $this->owner->getId()])
1732
                );
1733
            }
1734
1735
            $frmStudent = new FormValidator('frm_student_list', 'get');
1736
1737
            $urlParams = http_build_query(
1738
                [
1739
                    'a' => 'search_user_by_course',
1740
                    'course_id' => $this->course->getId(),
1741
                    'session_id' => $this->session ? $this->session->getId() : 0,
1742
                ]
1743
            );
1744
1745
            $frmStudent
1746
                ->addSelectAjax(
1747
                    'user',
1748
                    get_lang('SelectLearnerPortfolio'),
1749
                    [],
1750
                    [
1751
                        'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1752
                        'placeholder' => get_lang('SearchStudent'),
1753
                        'formatResult' => SelectAjax::templateResultForUsersInCourse(),
1754
                        'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(),
1755
                    ]
1756
                )
1757
                ->addOption(
1758
                    $this->owner->getCompleteName(),
1759
                    $this->owner->getId(),
1760
                    [
1761
                        'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()),
1762
                        'data-username' => $this->owner->getUsername(),
1763
                    ]
1764
                )
1765
            ;
1766
            $frmStudent->setDefaults(['user' => $this->owner->getId()]);
1767
            $frmStudent->addHidden('action', 'details');
1768
            $frmStudent->addHidden('cidReq', $this->course->getCode());
1769
            $frmStudent->addHidden('id_session', $this->session ? $this->session->getId() : 0);
1770
            $frmStudent->addButtonFilter(get_lang('Filter'));
1771
        }
1772
1773
        $itemsRepo = $this->em->getRepository(Portfolio::class);
1774
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
1775
1776
        $getItemsTotalNumber = function () use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) {
1777
            $qb = $itemsRepo->createQueryBuilder('i');
1778
            $qb
1779
                ->select('COUNT(i)')
1780
                ->where('i.user = :user')
1781
                ->setParameter('user', $this->owner);
1782
1783
            if ($this->course) {
1784
                $qb
1785
                    ->andWhere('i.course = :course')
1786
                    ->setParameter('course', $this->course);
1787
1788
                if ($this->session) {
1789
                    $qb
1790
                        ->andWhere('i.session = :session')
1791
                        ->setParameter('session', $this->session);
1792
                } else {
1793
                    $qb->andWhere('i.session IS NULL');
1794
                }
1795
            }
1796
1797
            if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
1798
                $visibilityCriteria = [
1799
                    Portfolio::VISIBILITY_VISIBLE,
1800
                    Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER,
1801
                ];
1802
1803
                $qb->andWhere(
1804
                    $qb->expr()->in('i.visibility', $visibilityCriteria)
1805
                );
1806
            }
1807
1808
            return $qb->getQuery()->getSingleScalarResult();
1809
        };
1810
        $getItemsData = function ($from, $limit, $columnNo, $orderDirection) use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) {
1811
            $qb = $itemsRepo->createQueryBuilder('item')
1812
                ->where('item.user = :user')
1813
                ->leftJoin('item.category', 'category')
1814
                ->leftJoin('item.course', 'course')
1815
                ->leftJoin('item.session', 'session')
1816
                ->setParameter('user', $this->owner);
1817
1818
            if ($this->course) {
1819
                $qb
1820
                    ->andWhere('item.course = :course_id')
1821
                    ->setParameter('course_id', $this->course);
1822
1823
                if ($this->session) {
1824
                    $qb
1825
                        ->andWhere('item.session = :session')
1826
                        ->setParameter('session', $this->session);
1827
                } else {
1828
                    $qb->andWhere('item.session IS NULL');
1829
                }
1830
            }
1831
1832
            if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
1833
                $visibilityCriteria = [
1834
                    Portfolio::VISIBILITY_VISIBLE,
1835
                    Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER,
1836
                ];
1837
1838
                $qb->andWhere(
1839
                    $qb->expr()->in('item.visibility', $visibilityCriteria)
1840
                );
1841
            }
1842
1843
            if (0 == $columnNo) {
1844
                $qb->orderBy('item.title', $orderDirection);
1845
            } elseif (1 == $columnNo) {
1846
                $qb->orderBy('item.creationDate', $orderDirection);
1847
            } elseif (2 == $columnNo) {
1848
                $qb->orderBy('item.updateDate', $orderDirection);
1849
            } elseif (3 == $columnNo) {
1850
                $qb->orderBy('category.title', $orderDirection);
1851
            } elseif (5 == $columnNo) {
1852
                $qb->orderBy('item.score', $orderDirection);
1853
            } elseif (6 == $columnNo) {
1854
                $qb->orderBy('course.title', $orderDirection);
1855
            } elseif (7 == $columnNo) {
1856
                $qb->orderBy('session.name', $orderDirection);
1857
            }
1858
1859
            $qb->setFirstResult($from)->setMaxResults($limit);
1860
1861
            return array_map(
1862
                function (Portfolio $item) {
1863
                    $category = $item->getCategory();
1864
1865
                    $row = [];
1866
                    $row[] = $item;
1867
                    $row[] = $item->getCreationDate();
1868
                    $row[] = $item->getUpdateDate();
1869
                    $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...
1870
                    $row[] = $item->getComments()->count();
1871
                    $row[] = $item->getScore();
1872
1873
                    if (!$this->course) {
1874
                        $row[] = $item->getCourse();
1875
                        $row[] = $item->getSession();
1876
                    }
1877
1878
                    return $row;
1879
                },
1880
                $qb->getQuery()->getResult()
1881
            );
1882
        };
1883
1884
        $portfolioItemColumnFilter = function (Portfolio $item) {
1885
            return Display::url(
1886
                $item->getTitle(true),
1887
                $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
1888
            );
1889
        };
1890
        $convertFormatDateColumnFilter = function (DateTime $date) {
1891
            return api_convert_and_format_date($date);
1892
        };
1893
1894
        $tblItems = new SortableTable('tbl_items', $getItemsTotalNumber, $getItemsData, 1, 20, 'DESC');
1895
        $tblItems->set_additional_parameters(['action' => 'details', 'user' => $this->owner->getId()]);
1896
        $tblItems->set_header(0, get_lang('Title'));
1897
        $tblItems->set_column_filter(0, $portfolioItemColumnFilter);
1898
        $tblItems->set_header(1, get_lang('CreationDate'), true, [], ['class' => 'text-center']);
1899
        $tblItems->set_column_filter(1, $convertFormatDateColumnFilter);
1900
        $tblItems->set_header(2, get_lang('LastUpdate'), true, [], ['class' => 'text-center']);
1901
        $tblItems->set_column_filter(2, $convertFormatDateColumnFilter);
1902
        $tblItems->set_header(3, get_lang('Category'));
1903
        $tblItems->set_header(4, get_lang('Comments'), false, [], ['class' => 'text-right']);
1904
        $tblItems->set_header(5, get_lang('Score'), true, [], ['class' => 'text-right']);
1905
1906
        if (!$this->course) {
1907
            $tblItems->set_header(6, get_lang('Course'));
1908
            $tblItems->set_header(7, get_lang('Session'));
1909
        }
1910
1911
        $getCommentsTotalNumber = function () use ($commentsRepo) {
1912
            $qb = $commentsRepo->createQueryBuilder('c');
1913
            $qb
1914
                ->select('COUNT(c)')
1915
                ->where('c.author = :author')
1916
                ->setParameter('author', $this->owner);
1917
1918
            if ($this->course) {
1919
                $qb
1920
                    ->innerJoin('c.item', 'i')
1921
                    ->andWhere('i.course = :course')
1922
                    ->setParameter('course', $this->course);
1923
1924
                if ($this->session) {
1925
                    $qb
1926
                        ->andWhere('i.session = :session')
1927
                        ->setParameter('session', $this->session);
1928
                } else {
1929
                    $qb->andWhere('i.session IS NULL');
1930
                }
1931
            }
1932
1933
            return $qb->getQuery()->getSingleScalarResult();
1934
        };
1935
        $getCommentsData = function ($from, $limit, $columnNo, $orderDirection) use ($commentsRepo) {
1936
            $qb = $commentsRepo->createQueryBuilder('comment');
1937
            $qb
1938
                ->where('comment.author = :user')
1939
                ->innerJoin('comment.item', 'item')
1940
                ->setParameter('user', $this->owner);
1941
1942
            if ($this->course) {
1943
                $qb
1944
                    ->innerJoin('comment.item', 'i')
1945
                    ->andWhere('item.course = :course')
1946
                    ->setParameter('course', $this->course);
1947
1948
                if ($this->session) {
1949
                    $qb
1950
                        ->andWhere('item.session = :session')
1951
                        ->setParameter('session', $this->session);
1952
                } else {
1953
                    $qb->andWhere('item.session IS NULL');
1954
                }
1955
            }
1956
1957
            if (0 == $columnNo) {
1958
                $qb->orderBy('comment.content', $orderDirection);
1959
            } elseif (1 == $columnNo) {
1960
                $qb->orderBy('comment.date', $orderDirection);
1961
            } elseif (2 == $columnNo) {
1962
                $qb->orderBy('item.title', $orderDirection);
1963
            } elseif (3 == $columnNo) {
1964
                $qb->orderBy('comment.score', $orderDirection);
1965
            }
1966
1967
            $qb->setFirstResult($from)->setMaxResults($limit);
1968
1969
            return array_map(
1970
                function (PortfolioComment $comment) {
1971
                    return [
1972
                        $comment,
1973
                        $comment->getDate(),
1974
                        $comment->getItem(),
1975
                        $comment->getScore(),
1976
                    ];
1977
                },
1978
                $qb->getQuery()->getResult()
1979
            );
1980
        };
1981
1982
        $tblComments = new SortableTable('tbl_comments', $getCommentsTotalNumber, $getCommentsData, 1, 20, 'DESC');
1983
        $tblComments->set_additional_parameters(['action' => 'details', 'user' => $this->owner->getId()]);
1984
        $tblComments->set_header(0, get_lang('Resume'));
1985
        $tblComments->set_column_filter(
1986
            0,
1987
            function (PortfolioComment $comment) {
1988
                return Display::url(
1989
                    $comment->getExcerpt(),
1990
                    $this->baseUrl.http_build_query(['action' => 'view', 'id' => $comment->getItem()->getId()])
1991
                    .'#comment-'.$comment->getId()
1992
                );
1993
            }
1994
        );
1995
        $tblComments->set_header(1, get_lang('Date'), true, [], ['class' => 'text-center']);
1996
        $tblComments->set_column_filter(1, $convertFormatDateColumnFilter);
1997
        $tblComments->set_header(2, get_lang('PortfolioItemTitle'));
1998
        $tblComments->set_column_filter(2, $portfolioItemColumnFilter);
1999
        $tblComments->set_header(3, get_lang('Score'), true, [], ['class' => 'text-right']);
2000
2001
        $content = '';
2002
2003
        if ($frmStudent) {
2004
            $content .= $frmStudent->returnForm();
2005
        }
2006
2007
        $totalNumberOfItems = $tblItems->get_total_number_of_items();
2008
        $totalNumberOfComments = $tblComments->get_total_number_of_items();
2009
        $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items');
2010
        $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments');
2011
2012
        $itemsSubtitle = '';
2013
2014
        if ($requiredNumberOfItems > 0) {
2015
            $itemsSubtitle = sprintf(
2016
                get_lang('XAddedYRequired'),
2017
                $totalNumberOfItems,
2018
                $requiredNumberOfItems
2019
            );
2020
        }
2021
2022
        $content .= Display::page_subheader2(
2023
            get_lang('PortfolioItems'),
2024
            $itemsSubtitle
2025
        ).PHP_EOL;
2026
2027
        if ($totalNumberOfItems > 0) {
2028
            $content .= $tblItems->return_table().PHP_EOL;
2029
        } else {
2030
            $content .= Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
2031
        }
2032
2033
        $commentsSubtitle = '';
2034
2035
        if ($requiredNumberOfComments > 0) {
2036
            $commentsSubtitle = sprintf(
2037
                get_lang('XAddedYRequired'),
2038
                $totalNumberOfComments,
2039
                $requiredNumberOfComments
2040
            );
2041
        }
2042
2043
        $content .= Display::page_subheader2(
2044
            get_lang('PortfolioCommentsMade'),
2045
            $commentsSubtitle
2046
        ).PHP_EOL;
2047
2048
        if ($totalNumberOfComments > 0) {
2049
            $content .= $tblComments->return_table().PHP_EOL;
2050
        } else {
2051
            $content .= Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
2052
        }
2053
2054
        $this->renderView($content, get_lang('PortfolioDetails'), $actions);
2055
    }
2056
2057
    /**
2058
     * @throws MpdfException
2059
     */
2060
    public function exportPdf(HttpRequest $httpRequest)
2061
    {
2062
        $currentUserId = api_get_user_id();
2063
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
2064
2065
        if ($isAllowedToFilterStudent) {
2066
            if ($httpRequest->query->has('user')) {
2067
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
2068
2069
                if (empty($this->owner)) {
2070
                    api_not_allowed(true);
2071
                }
2072
            }
2073
        }
2074
2075
        $pdfContent = Display::page_header($this->owner->getCompleteName());
2076
2077
        if ($this->course) {
2078
            $pdfContent .= '<p>'.get_lang('Course').': ';
2079
2080
            if ($this->session) {
2081
                $pdfContent .= $this->session->getName().' ('.$this->course->getTitle().')';
2082
            } else {
2083
                $pdfContent .= $this->course->getTitle();
2084
            }
2085
2086
            $pdfContent .= '</p>';
2087
        }
2088
2089
        $visibility = [];
2090
2091
        if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
2092
            $visibility[] = Portfolio::VISIBILITY_VISIBLE;
2093
            $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2094
        }
2095
2096
        $items = $this->em
2097
            ->getRepository(Portfolio::class)
2098
            ->findItemsByUser(
2099
                $this->owner,
2100
                $this->course,
2101
                $this->session,
2102
                null,
2103
                $visibility
2104
            );
2105
        $comments = $this->em
2106
            ->getRepository(PortfolioComment::class)
2107
            ->findCommentsByUser($this->owner, $this->course, $this->session);
2108
2109
        $itemsHtml = $this->getItemsInHtmlFormatted($items);
2110
        $commentsHtml = $this->getCommentsInHtmlFormatted($comments);
2111
2112
        $totalNumberOfItems = count($itemsHtml);
2113
        $totalNumberOfComments = count($commentsHtml);
2114
        $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items');
2115
        $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments');
2116
2117
        $itemsSubtitle = '';
2118
        $commentsSubtitle = '';
2119
2120
        if ($requiredNumberOfItems > 0) {
2121
            $itemsSubtitle = sprintf(
2122
                get_lang('XAddedYRequired'),
2123
                $totalNumberOfItems,
2124
                $requiredNumberOfItems
2125
            );
2126
        }
2127
2128
        if ($requiredNumberOfComments > 0) {
2129
            $commentsSubtitle = sprintf(
2130
                get_lang('XAddedYRequired'),
2131
                $totalNumberOfComments,
2132
                $requiredNumberOfComments
2133
            );
2134
        }
2135
2136
        $pdfContent .= Display::page_subheader2(
2137
            get_lang('PortfolioItems'),
2138
            $itemsSubtitle
2139
        );
2140
2141
        if ($totalNumberOfItems > 0) {
2142
            $pdfContent .= implode(PHP_EOL, $itemsHtml);
2143
        } else {
2144
            $pdfContent .= Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
2145
        }
2146
2147
        $pdfContent .= Display::page_subheader2(
2148
            get_lang('PortfolioCommentsMade'),
2149
            $commentsSubtitle
2150
        );
2151
2152
        if ($totalNumberOfComments > 0) {
2153
            $pdfContent .= implode(PHP_EOL, $commentsHtml);
2154
        } else {
2155
            $pdfContent .= Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
2156
        }
2157
2158
        $pdfName = $this->owner->getCompleteName()
2159
            .($this->course ? '_'.$this->course->getCode() : '')
2160
            .'_'.get_lang('Portfolio');
2161
2162
        $pdf = new PDF();
2163
        $pdf->content_to_pdf(
2164
            $pdfContent,
2165
            null,
2166
            $pdfName,
2167
            $this->course ? $this->course->getCode() : null,
2168
            'D',
2169
            false,
2170
            null,
2171
            false,
2172
            true
2173
        );
2174
    }
2175
2176
    public function exportZip(HttpRequest $httpRequest)
2177
    {
2178
        $currentUserId = api_get_user_id();
2179
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
2180
2181
        if ($isAllowedToFilterStudent) {
2182
            if ($httpRequest->query->has('user')) {
2183
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
2184
2185
                if (empty($this->owner)) {
2186
                    api_not_allowed(true);
2187
                }
2188
            }
2189
        }
2190
2191
        $itemsRepo = $this->em->getRepository(Portfolio::class);
2192
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
2193
        $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class);
2194
2195
        $visibility = [];
2196
2197
        if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
2198
            $visibility[] = Portfolio::VISIBILITY_VISIBLE;
2199
            $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2200
        }
2201
2202
        $items = $itemsRepo->findItemsByUser(
2203
            $this->owner,
2204
            $this->course,
2205
            $this->session,
2206
            null,
2207
            $visibility
2208
        );
2209
        $comments = $commentsRepo->findCommentsByUser($this->owner, $this->course, $this->session);
2210
2211
        $itemsHtml = $this->getItemsInHtmlFormatted($items);
2212
        $commentsHtml = $this->getCommentsInHtmlFormatted($comments);
2213
2214
        $sysArchivePath = api_get_path(SYS_ARCHIVE_PATH);
2215
        $tempPortfolioDirectory = $sysArchivePath."portfolio/{$this->owner->getId()}";
2216
2217
        $userDirectory = UserManager::getUserPathById($this->owner->getId(), 'system');
2218
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2219
2220
        $tblItemsHeaders = [];
2221
        $tblItemsHeaders[] = get_lang('Title');
2222
        $tblItemsHeaders[] = get_lang('CreationDate');
2223
        $tblItemsHeaders[] = get_lang('LastUpdate');
2224
        $tblItemsHeaders[] = get_lang('Category');
2225
        $tblItemsHeaders[] = get_lang('Category');
2226
        $tblItemsHeaders[] = get_lang('Score');
2227
        $tblItemsHeaders[] = get_lang('Course');
2228
        $tblItemsHeaders[] = get_lang('Session');
2229
        $tblItemsData = [];
2230
2231
        $tblCommentsHeaders = [];
2232
        $tblCommentsHeaders[] = get_lang('Resume');
2233
        $tblCommentsHeaders[] = get_lang('Date');
2234
        $tblCommentsHeaders[] = get_lang('PortfolioItemTitle');
2235
        $tblCommentsHeaders[] = get_lang('Score');
2236
        $tblCommentsData = [];
2237
2238
        $filenames = [];
2239
2240
        $fs = new Filesystem();
2241
2242
        /**
2243
         * @var int       $i
2244
         * @var Portfolio $item
2245
         */
2246
        foreach ($items as $i => $item) {
2247
            $itemCategory = $item->getCategory();
2248
            $itemCourse = $item->getCourse();
2249
            $itemSession = $item->getSession();
2250
2251
            $itemDirectory = $item->getCreationDate()->format('Y-m-d-H-i-s');
2252
2253
            $itemFilename = sprintf('%s/items/%s/item.html', $tempPortfolioDirectory, $itemDirectory);
2254
            $itemFileContent = $this->fixImagesSourcesToHtml($itemsHtml[$i]);
2255
2256
            $fs->dumpFile($itemFilename, $itemFileContent);
2257
2258
            $filenames[] = $itemFilename;
2259
2260
            $attachments = $attachmentsRepo->findFromItem($item);
2261
2262
            /** @var PortfolioAttachment $attachment */
2263
            foreach ($attachments as $attachment) {
2264
                $attachmentFilename = sprintf(
2265
                    '%s/items/%s/attachments/%s',
2266
                    $tempPortfolioDirectory,
2267
                    $itemDirectory,
2268
                    $attachment->getFilename()
2269
                );
2270
2271
                $fs->copy(
2272
                    $attachmentsDirectory.$attachment->getPath(),
2273
                    $attachmentFilename
2274
                );
2275
2276
                $filenames[] = $attachmentFilename;
2277
            }
2278
2279
            $tblItemsData[] = [
2280
                Display::url(
2281
                    Security::remove_XSS($item->getTitle()),
2282
                    sprintf('items/%s/item.html', $itemDirectory)
2283
                ),
2284
                api_convert_and_format_date($item->getCreationDate()),
2285
                api_convert_and_format_date($item->getUpdateDate()),
2286
                $itemCategory ? $itemCategory->getTitle() : null,
2287
                $item->getComments()->count(),
2288
                $item->getScore(),
2289
                $itemCourse->getTitle(),
2290
                $itemSession ? $itemSession->getName() : null,
2291
            ];
2292
        }
2293
2294
        /**
2295
         * @var int              $i
2296
         * @var PortfolioComment $comment
2297
         */
2298
        foreach ($comments as $i => $comment) {
2299
            $commentDirectory = $comment->getDate()->format('Y-m-d-H-i-s');
2300
2301
            $commentFileContent = $this->fixImagesSourcesToHtml($commentsHtml[$i]);
2302
            $commentFilename = sprintf('%s/comments/%s/comment.html', $tempPortfolioDirectory, $commentDirectory);
2303
2304
            $fs->dumpFile($commentFilename, $commentFileContent);
2305
2306
            $filenames[] = $commentFilename;
2307
2308
            $attachments = $attachmentsRepo->findFromComment($comment);
2309
2310
            /** @var PortfolioAttachment $attachment */
2311
            foreach ($attachments as $attachment) {
2312
                $attachmentFilename = sprintf(
2313
                    '%s/comments/%s/attachments/%s',
2314
                    $tempPortfolioDirectory,
2315
                    $commentDirectory,
2316
                    $attachment->getFilename()
2317
                );
2318
2319
                $fs->copy(
2320
                    $attachmentsDirectory.$attachment->getPath(),
2321
                    $attachmentFilename
2322
                );
2323
2324
                $filenames[] = $attachmentFilename;
2325
            }
2326
2327
            $tblCommentsData[] = [
2328
                Display::url(
2329
                    $comment->getExcerpt(),
2330
                    sprintf('comments/%s/comment.html', $commentDirectory)
2331
                ),
2332
                api_convert_and_format_date($comment->getDate()),
2333
                Security::remove_XSS($comment->getItem()->getTitle()),
2334
                $comment->getScore(),
2335
            ];
2336
        }
2337
2338
        $tblItems = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table']);
2339
        $tblItems->setHeaders($tblItemsHeaders);
2340
        $tblItems->setData($tblItemsData);
2341
2342
        $tblComments = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table']);
2343
        $tblComments->setHeaders($tblCommentsHeaders);
2344
        $tblComments->setData($tblCommentsData);
2345
2346
        $itemFilename = sprintf('%s/index.html', $tempPortfolioDirectory);
2347
2348
        $filenames[] = $itemFilename;
2349
2350
        $fs->dumpFile(
2351
            $itemFilename,
2352
            $this->formatZipIndexFile($tblItems, $tblComments)
2353
        );
2354
2355
        $zipName = $this->owner->getCompleteName()
2356
            .($this->course ? '_'.$this->course->getCode() : '')
2357
            .'_'.get_lang('Portfolio');
2358
        $tempZipFile = $sysArchivePath."portfolio/$zipName.zip";
2359
        $zip = new PclZip($tempZipFile);
2360
2361
        foreach ($filenames as $filename) {
2362
            $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...
2363
        }
2364
2365
        DocumentManager::file_send_for_download($tempZipFile, true, "$zipName.zip");
2366
2367
        $fs->remove($tempPortfolioDirectory);
2368
        $fs->remove($tempZipFile);
2369
    }
2370
2371
    public function qualifyItem(Portfolio $item)
2372
    {
2373
        global $interbreadcrumb;
2374
2375
        $em = Database::getManager();
2376
2377
        $formAction = $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()]);
2378
2379
        $form = new FormValidator('frm_qualify', 'post', $formAction);
2380
        $form->addUserAvatar('user', get_lang('Author'));
2381
        $form->addLabel(get_lang('Title'), $item->getTitle());
2382
2383
        $itemContent = $this->generateItemContent($item);
2384
2385
        $form->addLabel(get_lang('Content'), $itemContent);
2386
        $form->addNumeric(
2387
            'score',
2388
            [get_lang('QualifyNumeric'), null, ' / '.api_get_course_setting('portfolio_max_score')]
2389
        );
2390
        $form->addButtonSave(get_lang('QualifyThisPortfolioItem'));
2391
2392
        if ($form->validate()) {
2393
            $values = $form->exportValues();
2394
2395
            $item->setScore($values['score']);
2396
2397
            $em->persist($item);
2398
            $em->flush();
2399
2400
            Display::addFlash(
2401
                Display::return_message(get_lang('PortfolioItemGraded'), 'success')
2402
            );
2403
2404
            header("Location: $formAction");
2405
            exit();
2406
        }
2407
2408
        $form->setDefaults(
2409
            [
2410
                'user' => $item->getUser(),
2411
                'score' => (float) $item->getScore(),
2412
            ]
2413
        );
2414
2415
        $interbreadcrumb[] = [
2416
            'name' => get_lang('Portfolio'),
2417
            'url' => $this->baseUrl,
2418
        ];
2419
        $interbreadcrumb[] = [
2420
            'name' => $item->getTitle(true),
2421
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2422
        ];
2423
2424
        $actions = [];
2425
        $actions[] = Display::url(
2426
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2427
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
2428
        );
2429
2430
        $this->renderView($form->returnForm(), get_lang('Qualify'), $actions);
2431
    }
2432
2433
    public function qualifyComment(PortfolioComment $comment)
2434
    {
2435
        global $interbreadcrumb;
2436
2437
        $em = Database::getManager();
2438
2439
        $item = $comment->getItem();
2440
        $commentPath = $em->getRepository(PortfolioComment::class)->getPath($comment);
2441
2442
        $template = new Template('', false, false, false, true, false, false);
2443
        $template->assign('item', $item);
2444
        $template->assign('comments_path', $commentPath);
2445
        $commentContext = $template->fetch(
2446
            $template->get_template('portfolio/comment_context.html.twig')
2447
        );
2448
2449
        $formAction = $this->baseUrl.http_build_query(['action' => 'qualify', 'comment' => $comment->getId()]);
2450
2451
        $form = new FormValidator('frm_qualify', 'post', $formAction);
2452
        $form->addHtml($commentContext);
2453
        $form->addUserAvatar('user', get_lang('Author'));
2454
        $form->addLabel(get_lang('Comment'), $comment->getContent());
2455
        $form->addNumeric(
2456
            'score',
2457
            [get_lang('QualifyNumeric'), null, '/ '.api_get_course_setting('portfolio_max_score')]
2458
        );
2459
        $form->addButtonSave(get_lang('QualifyThisPortfolioComment'));
2460
2461
        if ($form->validate()) {
2462
            $values = $form->exportValues();
2463
2464
            $comment->setScore($values['score']);
2465
2466
            $em->persist($comment);
2467
            $em->flush();
2468
2469
            Display::addFlash(
2470
                Display::return_message(get_lang('PortfolioCommentGraded'), 'success')
2471
            );
2472
2473
            header("Location: $formAction");
2474
            exit();
2475
        }
2476
2477
        $form->setDefaults(
2478
            [
2479
                'user' => $comment->getAuthor(),
2480
                'score' => (float) $comment->getScore(),
2481
            ]
2482
        );
2483
2484
        $interbreadcrumb[] = [
2485
            'name' => get_lang('Portfolio'),
2486
            'url' => $this->baseUrl,
2487
        ];
2488
        $interbreadcrumb[] = [
2489
            'name' => $item->getTitle(true),
2490
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2491
        ];
2492
2493
        $actions = [];
2494
        $actions[] = Display::url(
2495
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2496
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
2497
        );
2498
2499
        $this->renderView($form->returnForm(), get_lang('Qualify'), $actions);
2500
    }
2501
2502
    public function downloadAttachment(HttpRequest $httpRequest)
2503
    {
2504
        $path = $httpRequest->query->get('file');
2505
2506
        if (empty($path)) {
2507
            api_not_allowed(true);
2508
        }
2509
2510
        $em = Database::getManager();
2511
        $attachmentRepo = $em->getRepository(PortfolioAttachment::class);
2512
2513
        $attachment = $attachmentRepo->findOneByPath($path);
2514
2515
        if (empty($attachment)) {
2516
            api_not_allowed(true);
2517
        }
2518
2519
        $originOwnerId = 0;
2520
2521
        if (PortfolioAttachment::TYPE_ITEM === $attachment->getOriginType()) {
2522
            $item = $em->find(Portfolio::class, $attachment->getOrigin());
2523
2524
            $originOwnerId = $item->getUser()->getId();
2525
        } elseif (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType()) {
2526
            $comment = $em->find(PortfolioComment::class, $attachment->getOrigin());
2527
2528
            $originOwnerId = $comment->getAuthor()->getId();
2529
        } else {
2530
            api_not_allowed(true);
2531
        }
2532
2533
        $userDirectory = UserManager::getUserPathById($originOwnerId, 'system');
2534
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2535
        $attachmentFilename = $attachmentsDirectory.$attachment->getPath();
2536
2537
        if (!Security::check_abs_path($attachmentFilename, $attachmentsDirectory)) {
2538
            api_not_allowed(true);
2539
        }
2540
2541
        $downloaded = DocumentManager::file_send_for_download(
2542
            $attachmentFilename,
2543
            true,
2544
            $attachment->getFilename()
2545
        );
2546
2547
        if (!$downloaded) {
2548
            api_not_allowed(true);
2549
        }
2550
    }
2551
2552
    public function deleteAttachment(HttpRequest $httpRequest)
2553
    {
2554
        $currentUserId = api_get_user_id();
2555
2556
        $path = $httpRequest->query->get('file');
2557
2558
        if (empty($path)) {
2559
            api_not_allowed(true);
2560
        }
2561
2562
        $em = Database::getManager();
2563
        $fs = new Filesystem();
2564
2565
        $attachmentRepo = $em->getRepository(PortfolioAttachment::class);
2566
        $attachment = $attachmentRepo->findOneByPath($path);
2567
2568
        if (empty($attachment)) {
2569
            api_not_allowed(true);
2570
        }
2571
2572
        $originOwnerId = 0;
2573
        $itemId = 0;
2574
2575
        if (PortfolioAttachment::TYPE_ITEM === $attachment->getOriginType()) {
2576
            $item = $em->find(Portfolio::class, $attachment->getOrigin());
2577
            $originOwnerId = $item->getUser()->getId();
2578
            $itemId = $item->getId();
2579
        } elseif (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType()) {
2580
            $comment = $em->find(PortfolioComment::class, $attachment->getOrigin());
2581
            $originOwnerId = $comment->getAuthor()->getId();
2582
            $itemId = $comment->getItem()->getId();
2583
        }
2584
2585
        if ($currentUserId !== $originOwnerId) {
2586
            api_not_allowed(true);
2587
        }
2588
2589
        $em->remove($attachment);
2590
        $em->flush();
2591
2592
        $userDirectory = UserManager::getUserPathById($originOwnerId, 'system');
2593
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2594
        $attachmentFilename = $attachmentsDirectory.$attachment->getPath();
2595
2596
        $fs->remove($attachmentFilename);
2597
2598
        if ($httpRequest->isXmlHttpRequest()) {
2599
            echo Display::return_message(get_lang('AttachmentFileDeleteSuccess'), 'success');
2600
        } else {
2601
            Display::addFlash(
2602
                Display::return_message(get_lang('AttachmentFileDeleteSuccess'), 'success')
2603
            );
2604
2605
            $url = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $itemId]);
2606
2607
            if (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType() && isset($comment)) {
2608
                $url .= '#comment-'.$comment->getId();
2609
            }
2610
2611
            header("Location: $url");
2612
        }
2613
2614
        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...
2615
    }
2616
2617
    /**
2618
     * @throws \Doctrine\ORM\OptimisticLockException
2619
     * @throws \Doctrine\ORM\ORMException
2620
     */
2621
    public function markAsHighlighted(Portfolio $item)
2622
    {
2623
        if ($item->getCourse()->getId() !== (int) api_get_course_int_id()) {
2624
            api_not_allowed(true);
2625
        }
2626
2627
        $item->setIsHighlighted(
2628
            !$item->isHighlighted()
2629
        );
2630
2631
        Database::getManager()->flush();
2632
2633
        Display::addFlash(
2634
            Display::return_message(
2635
                $item->isHighlighted() ? get_lang('MarkedAsHighlighted') : get_lang('UnmarkedAsHighlighted'),
2636
                'success'
2637
            )
2638
        );
2639
2640
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
2641
        exit;
2642
    }
2643
2644
    public function markAsTemplate(Portfolio $item)
2645
    {
2646
        if (!$this->itemBelongToOwner($item)) {
2647
            api_not_allowed(true);
2648
        }
2649
2650
        $item->setIsTemplate(
2651
            !$item->isTemplate()
2652
        );
2653
2654
        Database::getManager()->flush($item);
2655
2656
        Display::addFlash(
2657
            Display::return_message(
2658
                $item->isTemplate() ? get_lang('PortfolioItemSetAsTemplate') : get_lang('PortfolioItemUnsetAsTemplate'),
2659
                'success'
2660
            )
2661
        );
2662
2663
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
2664
        exit;
2665
    }
2666
2667
    public function markAsTemplateComment(PortfolioComment $comment)
2668
    {
2669
        if (!$this->commentBelongsToOwner($comment)) {
2670
            api_not_allowed(true);
2671
        }
2672
2673
        $comment->setIsTemplate(
2674
            !$comment->isTemplate()
2675
        );
2676
2677
        Database::getManager()->flush();
2678
2679
        Display::addFlash(
2680
            Display::return_message(
2681
                $comment->isTemplate() ? get_lang('PortfolioCommentSetAsTemplate') : get_lang('PortfolioCommentUnsetAsTemplate'),
2682
                'success'
2683
            )
2684
        );
2685
2686
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $comment->getItem()->getId()]));
2687
        exit;
2688
    }
2689
2690
    public function listTags(HttpRequest $request)
2691
    {
2692
        global $interbreadcrumb;
2693
2694
        api_protect_course_script();
2695
        api_protect_teacher_script();
2696
2697
        $em = Database::getManager();
2698
        $tagRepo = $em->getRepository(Tag::class);
2699
2700
        $tagsQuery = $tagRepo->findForPortfolioInCourseQuery($this->course, $this->session);
2701
2702
        $tag = $request->query->has('id')
2703
            ? $tagRepo->find($request->query->getInt('id'))
2704
            : null;
2705
2706
        $formAction = ['action' => $request->query->get('action')];
2707
2708
        if ($tag) {
2709
            $formAction['id'] = $tag->getId();
2710
        }
2711
2712
        $form = new FormValidator('frm_add_tag', 'post', $this->baseUrl.http_build_query($formAction));
2713
        $form->addText('name', get_lang('Tag'));
2714
2715
        if ($tag) {
2716
            $form->addButtonUpdate(get_lang('Edit'));
2717
        } else {
2718
            $form->addButtonCreate(get_lang('Add'));
2719
        }
2720
2721
        if ($form->validate()) {
2722
            $values = $form->exportValues();
2723
2724
            $extraFieldInfo = (new ExtraField('portfolio'))->get_handler_field_info_by_field_variable('tags');
2725
2726
            if (!$tag) {
2727
                $tag = (new Tag())->setCount(0);
2728
2729
                $portfolioRelTag = (new PortfolioRelTag())
2730
                    ->setTag($tag)
2731
                    ->setCourse($this->course)
2732
                    ->setSession($this->session)
2733
                ;
2734
2735
                $em->persist($tag);
2736
                $em->persist($portfolioRelTag);
2737
            }
2738
2739
            $tag
2740
                ->setTag($values['name'])
2741
                ->setFieldId((int) $extraFieldInfo['id'])
2742
            ;
2743
2744
            $em->flush();
2745
2746
            Display::addFlash(
2747
                Display::return_message(get_lang('TagSaved'), 'success')
2748
            );
2749
2750
            header('Location: '.$this->baseUrl.http_build_query($formAction));
2751
            exit();
2752
        } else {
2753
            $form->protect();
2754
2755
            if ($tag) {
2756
                $form->setDefaults(['name' => $tag->getTag()]);
2757
            }
2758
        }
2759
2760
        $langTags = get_lang('Tags');
2761
        $langEdit = get_lang('Edit');
2762
2763
        $deleteIcon = Display::return_icon('delete.png', get_lang('Delete'));
2764
        $editIcon = Display::return_icon('edit.png', $langEdit);
2765
2766
        $table = new SortableTable(
2767
            'portfolio_tags',
2768
            function () use ($tagsQuery) {
2769
                return (int) $tagsQuery
2770
                    ->select('COUNT(t)')
2771
                    ->getQuery()
2772
                    ->getSingleScalarResult()
2773
                ;
2774
            },
2775
            function ($from, $limit, $column, $direction) use ($tagsQuery) {
2776
                $data = [];
2777
2778
                /** @var array<int, Tag> $tags */
2779
                $tags = $tagsQuery
2780
                    ->select('t')
2781
                    ->orderBy('t.tag', $direction)
2782
                    ->setFirstResult($from)
2783
                    ->setMaxResults($limit)
2784
                    ->getQuery()
2785
                    ->getResult();
2786
2787
                foreach ($tags as $tag) {
2788
                    $data[] = [
2789
                        $tag->getTag(),
2790
                        $tag->getId(),
2791
                    ];
2792
                }
2793
2794
                return $data;
2795
            },
2796
            0,
2797
            40
2798
        );
2799
        $table->set_header(0, get_lang('Name'));
2800
        $table->set_header(1, get_lang('Actions'), false, ['class' => 'text-right'], ['class' => 'text-right']);
2801
        $table->set_column_filter(
2802
            1,
2803
            function ($id) use ($editIcon, $deleteIcon) {
2804
                $editParams = http_build_query(['action' => 'edit_tag', 'id' => $id]);
2805
                $deleteParams = http_build_query(['action' => 'delete_tag', 'id' => $id]);
2806
2807
                return Display::url($editIcon, $this->baseUrl.$editParams).PHP_EOL
2808
                    .Display::url($deleteIcon, $this->baseUrl.$deleteParams).PHP_EOL;
2809
            }
2810
        );
2811
        $table->set_additional_parameters(
2812
            [
2813
                'action' => 'tags',
2814
                'cidReq' => $this->course->getCode(),
2815
                'id_session' => $this->session ? $this->session->getId() : 0,
2816
                'gidReq' => 0,
2817
            ]
2818
        );
2819
2820
        $content = $form->returnForm().PHP_EOL
2821
            .$table->return_table();
2822
2823
        $interbreadcrumb[] = [
2824
            'name' => get_lang('Portfolio'),
2825
            'url' => $this->baseUrl,
2826
        ];
2827
2828
        $pageTitle = $langTags;
2829
2830
        if ($tag) {
2831
            $pageTitle = $langEdit;
2832
2833
            $interbreadcrumb[] = [
2834
                'name' => $langTags,
2835
                'url' => $this->baseUrl.'action=tags',
2836
            ];
2837
        }
2838
2839
        $this->renderView($content, $pageTitle);
2840
    }
2841
2842
    public function deleteTag(Tag $tag)
2843
    {
2844
        api_protect_course_script();
2845
        api_protect_teacher_script();
2846
2847
        $em = Database::getManager();
2848
        $portfolioTagRepo = $em->getRepository(PortfolioRelTag::class);
2849
2850
        $portfolioTag = $portfolioTagRepo
2851
            ->findOneBy(['tag' => $tag, 'course' => $this->course, 'session' => $this->session]);
2852
2853
        if ($portfolioTag) {
2854
            $em->remove($portfolioTag);
2855
            $em->flush();
2856
2857
            Display::addFlash(
2858
                Display::return_message(get_lang('TagDeleted'), 'success')
2859
            );
2860
        }
2861
2862
        header('Location: '.$this->baseUrl.http_build_query(['action' => 'tags']));
2863
        exit();
2864
    }
2865
2866
    /**
2867
     * @throws \Doctrine\ORM\OptimisticLockException
2868
     * @throws \Doctrine\ORM\ORMException
2869
     */
2870
    public function editComment(PortfolioComment $comment)
2871
    {
2872
        global $interbreadcrumb;
2873
2874
        if (!$this->commentBelongsToOwner($comment)) {
2875
            api_not_allowed(true);
2876
        }
2877
2878
        $item = $comment->getItem();
2879
        $commmentCourse = $item->getCourse();
2880
        $commmentSession = $item->getSession();
2881
2882
        $formAction = $this->baseUrl.http_build_query(['action' => 'edit_comment', 'id' => $comment->getId()]);
2883
2884
        $form = new FormValidator('frm_comment', 'post', $formAction);
2885
        $form->addLabel(
2886
            get_lang('Date'),
2887
            $this->getLabelForCommentDate($comment)
2888
        );
2889
        $form->addHtmlEditor('content', get_lang('Comments'), true, false, ['ToolbarSet' => 'Minimal']);
2890
        $form->applyFilter('content', 'trim');
2891
2892
        $this->addAttachmentsFieldToForm($form);
2893
2894
        $form->addButtonUpdate(get_lang('Update'));
2895
2896
        if ($form->validate()) {
2897
            if ($commmentCourse) {
0 ignored issues
show
introduced by
$commmentCourse is of type Chamilo\CoreBundle\Entity\Course, thus it always evaluated to true.
Loading history...
2898
                api_item_property_update(
2899
                    api_get_course_info($commmentCourse->getCode()),
2900
                    TOOL_PORTFOLIO,
2901
                    $comment->getId(),
2902
                    'PortfolioCommentUpdated',
2903
                    api_get_user_id(),
2904
                    [],
2905
                    null,
2906
                    '',
2907
                    '',
2908
                    $commmentSession ? $commmentSession->getId() : 0
0 ignored issues
show
introduced by
$commmentSession is of type Chamilo\CoreBundle\Entity\Session, thus it always evaluated to true.
Loading history...
2909
                );
2910
            }
2911
2912
            $values = $form->exportValues();
2913
2914
            $comment->setContent($values['content']);
2915
2916
            $this->em->flush();
2917
2918
            $this->processAttachments(
2919
                $form,
2920
                $comment->getAuthor(),
2921
                $comment->getId(),
2922
                PortfolioAttachment::TYPE_COMMENT
2923
            );
2924
2925
            Display::addFlash(
2926
                Display::return_message(get_lang('ItemUpdated'), 'success')
2927
            );
2928
2929
            header("Location: $this->baseUrl"
2930
                .http_build_query(['action' => 'view', 'id' => $item->getId()])
2931
                .'#comment-'.$comment->getId()
2932
            );
2933
            exit;
2934
        }
2935
2936
        $form->setDefaults([
2937
            'content' => $comment->getContent(),
2938
        ]);
2939
2940
        $interbreadcrumb[] = [
2941
            'name' => get_lang('Portfolio'),
2942
            'url' => $this->baseUrl,
2943
        ];
2944
        $interbreadcrumb[] = [
2945
            'name' => $item->getTitle(true),
2946
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2947
        ];
2948
2949
        $actions = [];
2950
        $actions[] = Display::url(
2951
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2952
            $this->baseUrl
2953
        );
2954
2955
        $content = $form->returnForm()
2956
            .PHP_EOL
2957
            .'<div class="row"> <div class="col-sm-8 col-sm-offset-2">'
2958
            .$this->generateAttachmentList($comment)
2959
            .'</div></div>';
2960
2961
        $this->renderView(
2962
            $content,
2963
            get_lang('EditPortfolioComment'),
2964
            $actions
2965
        );
2966
    }
2967
2968
    /**
2969
     * @throws \Doctrine\ORM\OptimisticLockException
2970
     * @throws \Doctrine\ORM\ORMException
2971
     */
2972
    public function deleteComment(PortfolioComment $comment)
2973
    {
2974
        if (!$this->commentBelongsToOwner($comment)) {
2975
            api_not_allowed(true);
2976
        }
2977
2978
        $this->em->remove($comment);
2979
2980
        $this->em
2981
            ->getRepository(PortfolioAttachment::class)
2982
            ->removeFromComment($comment);
2983
2984
        $this->em->flush();
2985
2986
        Display::addFlash(
2987
            Display::return_message(get_lang('CommentDeleted'), 'success')
2988
        );
2989
2990
        header("Location: $this->baseUrl");
2991
        exit;
2992
    }
2993
2994
    private function isAllowed(): bool
2995
    {
2996
        $isSubscribedInCourse = false;
2997
2998
        if ($this->course) {
2999
            $isSubscribedInCourse = CourseManager::is_user_subscribed_in_course(
3000
                api_get_user_id(),
3001
                $this->course->getCode(),
3002
                (bool) $this->session,
3003
                $this->session ? $this->session->getId() : 0
3004
            );
3005
        }
3006
3007
        if (!$this->course || $isSubscribedInCourse) {
3008
            return true;
3009
        }
3010
3011
        return false;
3012
    }
3013
3014
    private function blockIsNotAllowed()
3015
    {
3016
        if (!$this->isAllowed()) {
3017
            api_not_allowed(true);
3018
        }
3019
    }
3020
3021
    /**
3022
     * @param bool $showHeader
3023
     */
3024
    private function renderView(string $content, string $toolName, array $actions = [], $showHeader = true)
3025
    {
3026
        global $this_section;
3027
3028
        $this_section = $this->course ? SECTION_COURSES : SECTION_SOCIAL;
3029
3030
        $view = new Template($toolName);
3031
3032
        if ($showHeader) {
3033
            $view->assign('header', $toolName);
3034
        }
3035
3036
        $actionsStr = '';
3037
3038
        if ($this->course) {
3039
            $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...
3040
        }
3041
3042
        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...
3043
            $actions = implode('', $actions);
3044
3045
            $actionsStr .= Display::toolbarAction('portfolio-toolbar', [$actions]);
3046
        }
3047
3048
        $view->assign('baseurl', $this->baseUrl);
3049
        $view->assign('actions', $actionsStr);
3050
3051
        $view->assign('content', $content);
3052
        $view->display_one_col_template();
3053
    }
3054
3055
    private function categoryBelongToOwner(PortfolioCategory $category): bool
3056
    {
3057
        if ($category->getUser()->getId() != $this->owner->getId()) {
3058
            return false;
3059
        }
3060
3061
        return true;
3062
    }
3063
3064
    private function addAttachmentsFieldToForm(FormValidator $form)
3065
    {
3066
        $form->addButton('add_attachment', get_lang('AddAttachment'), 'plus');
3067
        $form->addHtml('<div id="container-attachments" style="display: none;">');
3068
        $form->addFile('attachment_file[]', get_lang('FilesAttachment'));
3069
        $form->addText('attachment_comment[]', get_lang('Description'), false);
3070
        $form->addHtml('</div>');
3071
3072
        $script = "$(function () {
3073
            var attachmentsTemplate = $('#container-attachments').html();
3074
            var \$btnAdd = $('[name=\"add_attachment\"]');
3075
            var \$reference = \$btnAdd.parents('.form-group');
3076
3077
            \$btnAdd.on('click', function (e) {
3078
                e.preventDefault();
3079
3080
                $(attachmentsTemplate).insertBefore(\$reference);
3081
            });
3082
        })";
3083
3084
        $form->addHtml("<script>$script</script>");
3085
    }
3086
3087
    private function processAttachments(
3088
        FormValidator $form,
3089
        User $user,
3090
        int $originId,
3091
        int $originType
3092
    ) {
3093
        $em = Database::getManager();
3094
        $fs = new Filesystem();
3095
3096
        $comments = $form->getSubmitValue('attachment_comment');
3097
3098
        foreach ($_FILES['attachment_file']['error'] as $i => $attachmentFileError) {
3099
            if ($attachmentFileError != UPLOAD_ERR_OK) {
3100
                continue;
3101
            }
3102
3103
            $_file = [
3104
                'name' => $_FILES['attachment_file']['name'][$i],
3105
                'type' => $_FILES['attachment_file']['type'][$i],
3106
                'tmp_name' => $_FILES['attachment_file']['tmp_name'][$i],
3107
                'size' => $_FILES['attachment_file']['size'][$i],
3108
            ];
3109
3110
            if (empty($_file['type'])) {
3111
                $_file['type'] = DocumentManager::file_get_mime_type($_file['name']);
3112
            }
3113
3114
            $newFileName = add_ext_on_mime(stripslashes($_file['name']), $_file['type']);
3115
3116
            if (!filter_extension($newFileName)) {
3117
                Display::addFlash(Display::return_message(get_lang('UplUnableToSaveFileFilteredExtension'), 'error'));
3118
                continue;
3119
            }
3120
3121
            $newFileName = uniqid();
3122
            $attachmentsDirectory = UserManager::getUserPathById($user->getId(), 'system').'portfolio_attachments/';
3123
3124
            if (!$fs->exists($attachmentsDirectory)) {
3125
                $fs->mkdir($attachmentsDirectory, api_get_permissions_for_new_directories());
3126
            }
3127
3128
            $attachmentFilename = $attachmentsDirectory.$newFileName;
3129
3130
            if (is_uploaded_file($_file['tmp_name'])) {
3131
                $moved = move_uploaded_file($_file['tmp_name'], $attachmentFilename);
3132
3133
                if (!$moved) {
3134
                    Display::addFlash(Display::return_message(get_lang('UplUnableToSaveFile'), 'error'));
3135
                    continue;
3136
                }
3137
            }
3138
3139
            $attachment = new PortfolioAttachment();
3140
            $attachment
3141
                ->setFilename($_file['name'])
3142
                ->setComment($comments[$i])
3143
                ->setPath($newFileName)
3144
                ->setOrigin($originId)
3145
                ->setOriginType($originType)
3146
                ->setSize($_file['size']);
3147
3148
            $em->persist($attachment);
3149
            $em->flush();
3150
        }
3151
    }
3152
3153
    private function itemBelongToOwner(Portfolio $item): bool
3154
    {
3155
        if ($item->getUser()->getId() != $this->owner->getId()) {
3156
            return false;
3157
        }
3158
3159
        return true;
3160
    }
3161
3162
    private function commentBelongsToOwner(PortfolioComment $comment): bool
3163
    {
3164
        return $comment->getAuthor() === $this->owner;
3165
    }
3166
3167
    private function createFormTagFilter(bool $listByUser = false): FormValidator
3168
    {
3169
        $tags = Database::getManager()
3170
            ->getRepository(Tag::class)
3171
            ->findForPortfolioInCourseQuery($this->course, $this->session)
3172
            ->getQuery()
3173
            ->getResult()
3174
        ;
3175
3176
        $frmTagList = new FormValidator(
3177
            'frm_tag_list',
3178
            'get',
3179
            $this->baseUrl.($listByUser ? 'user='.$this->owner->getId() : ''),
3180
            '',
3181
            [],
3182
            FormValidator::LAYOUT_BOX
3183
        );
3184
3185
        $frmTagList->addDatePicker('date', get_lang('CreationDate'));
3186
3187
        $frmTagList->addSelectFromCollection(
3188
            'tags',
3189
            get_lang('Tags'),
3190
            $tags,
3191
            ['multiple' => 'multiple'],
3192
            false,
3193
            'getTag'
3194
        );
3195
3196
        $frmTagList->addText('text', get_lang('Search'), false)->setIcon('search');
3197
        $frmTagList->applyFilter('text', 'trim');
3198
        $frmTagList->addHtml('<br>');
3199
        $frmTagList->addButtonFilter(get_lang('Filter'));
3200
3201
        if ($this->course) {
3202
            $frmTagList->addHidden('cidReq', $this->course->getCode());
3203
            $frmTagList->addHidden('id_session', $this->session ? $this->session->getId() : 0);
3204
            $frmTagList->addHidden('gidReq', 0);
3205
            $frmTagList->addHidden('gradebook', 0);
3206
            $frmTagList->addHidden('origin', '');
3207
            $frmTagList->addHidden('categoryId', 0);
3208
            $frmTagList->addHidden('subCategoryIds', '');
3209
3210
            if ($listByUser) {
3211
                $frmTagList->addHidden('user', $this->owner->getId());
3212
            }
3213
        }
3214
3215
        return $frmTagList;
3216
    }
3217
3218
    /**
3219
     * @throws Exception
3220
     */
3221
    private function createFormStudentFilter(bool $listByUser = false, bool $listHighlighted = false): FormValidator
3222
    {
3223
        $frmStudentList = new FormValidator(
3224
            'frm_student_list',
3225
            'get',
3226
            $this->baseUrl,
3227
            '',
3228
            [],
3229
            FormValidator::LAYOUT_BOX
3230
        );
3231
3232
        $urlParams = http_build_query(
3233
            [
3234
                'a' => 'search_user_by_course',
3235
                'course_id' => $this->course->getId(),
3236
                'session_id' => $this->session ? $this->session->getId() : 0,
3237
            ]
3238
        );
3239
3240
        /** @var SelectAjax $slctUser */
3241
        $slctUser = $frmStudentList->addSelectAjax(
3242
            'user',
3243
            get_lang('SelectLearnerPortfolio'),
3244
            [],
3245
            [
3246
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
3247
                'placeholder' => get_lang('SearchStudent'),
3248
                'formatResult' => SelectAjax::templateResultForUsersInCourse(),
3249
                'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(),
3250
            ]
3251
        );
3252
3253
        if ($listByUser) {
3254
            $slctUser->addOption(
3255
                $this->owner->getCompleteName(),
3256
                $this->owner->getId(),
3257
                [
3258
                    'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()),
3259
                    'data-username' => $this->owner->getUsername(),
3260
                ]
3261
            );
3262
3263
            $link = Display::url(
3264
                get_lang('BackToMainPortfolio'),
3265
                $this->baseUrl
3266
            );
3267
        } else {
3268
            $link = Display::url(
3269
                get_lang('SeeMyPortfolio'),
3270
                $this->baseUrl.http_build_query(['user' => api_get_user_id()])
3271
            );
3272
        }
3273
3274
        $frmStudentList->addHtml("<p>$link</p>");
3275
3276
        if ($listHighlighted) {
3277
            $link = Display::url(
3278
                get_lang('BackToMainPortfolio'),
3279
                $this->baseUrl
3280
            );
3281
        } else {
3282
            $link = Display::url(
3283
                get_lang('SeeHighlights'),
3284
                $this->baseUrl.http_build_query(['list_highlighted' => true])
3285
            );
3286
        }
3287
3288
        $frmStudentList->addHtml("<p>$link</p>");
3289
3290
        return $frmStudentList;
3291
    }
3292
3293
    private function getCategoriesForIndex(?int $currentUserId = null, ?int $parentId = null): array
3294
    {
3295
        $categoriesCriteria = [];
3296
        if (isset($currentUserId)) {
3297
            $categoriesCriteria['user'] = $this->owner;
3298
        }
3299
        if (!api_is_platform_admin() && $currentUserId !== $this->owner->getId()) {
3300
            $categoriesCriteria['isVisible'] = true;
3301
        }
3302
        if (isset($parentId)) {
3303
            $categoriesCriteria['parentId'] = $parentId;
3304
        }
3305
3306
        return $this->em
3307
            ->getRepository(PortfolioCategory::class)
3308
            ->findBy($categoriesCriteria);
3309
    }
3310
3311
    private function getHighlightedItems()
3312
    {
3313
        $queryBuilder = $this->em->createQueryBuilder();
3314
        $queryBuilder
3315
            ->select('pi')
3316
            ->from(Portfolio::class, 'pi')
3317
            ->where('pi.course = :course')
3318
            ->andWhere('pi.isHighlighted = TRUE')
3319
            ->setParameter('course', $this->course);
3320
3321
        if ($this->session) {
3322
            $queryBuilder->andWhere('pi.session = :session');
3323
            $queryBuilder->setParameter('session', $this->session);
3324
        } else {
3325
            $queryBuilder->andWhere('pi.session IS NULL');
3326
        }
3327
3328
        $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE];
3329
3330
        if (api_is_allowed_to_edit()) {
3331
            $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
3332
        }
3333
3334
        $queryBuilder
3335
            ->andWhere(
3336
                $queryBuilder->expr()->orX(
3337
                    'pi.user = :current_user',
3338
                    $queryBuilder->expr()->andX(
3339
                        'pi.user != :current_user',
3340
                        $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria)
3341
                    )
3342
                )
3343
            )
3344
            ->setParameter('current_user', api_get_user_id());
3345
3346
        $queryBuilder->orderBy('pi.creationDate', 'DESC');
3347
3348
        return $queryBuilder->getQuery()->getResult();
3349
    }
3350
3351
    private function getItemsForIndex(
3352
        bool $listByUser = false,
3353
        FormValidator $frmFilterList = null
3354
    ) {
3355
        $currentUserId = api_get_user_id();
3356
3357
        if ($this->course) {
3358
            $queryBuilder = $this->em->createQueryBuilder();
3359
            $queryBuilder
3360
                ->select('pi')
3361
                ->from(Portfolio::class, 'pi')
3362
                ->where('pi.course = :course');
3363
3364
            $queryBuilder->setParameter('course', $this->course);
3365
3366
            if ($this->session) {
3367
                $queryBuilder->andWhere('pi.session = :session');
3368
                $queryBuilder->setParameter('session', $this->session);
3369
            } else {
3370
                $queryBuilder->andWhere('pi.session IS NULL');
3371
            }
3372
3373
            if ($frmFilterList && $frmFilterList->validate()) {
3374
                $values = $frmFilterList->exportValues();
3375
3376
                if (!empty($values['date'])) {
3377
                    $queryBuilder
3378
                        ->andWhere('pi.creationDate >= :date')
3379
                        ->setParameter(':date', api_get_utc_datetime($values['date'], false, true))
3380
                    ;
3381
                }
3382
3383
                if (!empty($values['tags'])) {
3384
                    $queryBuilder
3385
                        ->innerJoin(ExtraFieldRelTag::class, 'efrt', Join::WITH, 'efrt.itemId = pi.id')
3386
                        ->innerJoin(ExtraFieldEntity::class, 'ef', Join::WITH, 'ef.id = efrt.fieldId')
3387
                        ->andWhere('ef.extraFieldType = :efType')
3388
                        ->andWhere('ef.variable = :variable')
3389
                        ->andWhere('efrt.tagId IN (:tags)');
3390
3391
                    $queryBuilder->setParameter('efType', ExtraFieldEntity::PORTFOLIO_TYPE);
3392
                    $queryBuilder->setParameter('variable', 'tags');
3393
                    $queryBuilder->setParameter('tags', $values['tags']);
3394
                }
3395
3396
                if (!empty($values['text'])) {
3397
                    $queryBuilder->andWhere(
3398
                        $queryBuilder->expr()->orX(
3399
                            $queryBuilder->expr()->like('pi.title', ':text'),
3400
                            $queryBuilder->expr()->like('pi.content', ':text')
3401
                        )
3402
                    );
3403
3404
                    $queryBuilder->setParameter('text', '%'.$values['text'].'%');
3405
                }
3406
3407
                // Filters by category level 0
3408
                $searchCategories = [];
3409
                if (!empty($values['categoryId'])) {
3410
                    $searchCategories[] = $values['categoryId'];
3411
                    $subCategories = $this->getCategoriesForIndex(null, $values['categoryId']);
3412
                    if (count($subCategories) > 0) {
3413
                        foreach ($subCategories as $subCategory) {
3414
                            $searchCategories[] = $subCategory->getId();
3415
                        }
3416
                    }
3417
                    $queryBuilder->andWhere('pi.category IN('.implode(',', $searchCategories).')');
3418
                }
3419
3420
                // Filters by sub-category, don't show the selected values
3421
                $diff = [];
3422
                if (!empty($values['subCategoryIds']) && !('all' === $values['subCategoryIds'])) {
3423
                    $subCategoryIds = explode(',', $values['subCategoryIds']);
3424
                    $diff = array_diff($searchCategories, $subCategoryIds);
3425
                } else {
3426
                    if (trim($values['subCategoryIds']) === '') {
3427
                        $diff = $searchCategories;
3428
                    }
3429
                }
3430
                if (!empty($diff)) {
3431
                    unset($diff[0]);
3432
                    if (!empty($diff)) {
3433
                        $queryBuilder->andWhere('pi.category NOT IN('.implode(',', $diff).')');
3434
                    }
3435
                }
3436
            }
3437
3438
            if ($listByUser) {
3439
                $queryBuilder
3440
                    ->andWhere('pi.user = :user')
3441
                    ->setParameter('user', $this->owner);
3442
            }
3443
3444
            $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE];
3445
3446
            if (api_is_allowed_to_edit()) {
3447
                $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
3448
            }
3449
3450
            $queryBuilder
3451
                ->andWhere(
3452
                    $queryBuilder->expr()->orX(
3453
                        'pi.user = :current_user',
3454
                        $queryBuilder->expr()->andX(
3455
                            'pi.user != :current_user',
3456
                            $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria)
3457
                        )
3458
                    )
3459
                )
3460
                ->setParameter('current_user', $currentUserId);
3461
3462
            $queryBuilder->orderBy('pi.creationDate', 'DESC');
3463
3464
            $items = $queryBuilder->getQuery()->getResult();
3465
        } else {
3466
            $itemsCriteria = [];
3467
            $itemsCriteria['category'] = null;
3468
            $itemsCriteria['user'] = $this->owner;
3469
3470
            if ($currentUserId !== $this->owner->getId()) {
3471
                $itemsCriteria['visibility'] = Portfolio::VISIBILITY_VISIBLE;
3472
            }
3473
3474
            $items = $this->em
3475
                ->getRepository(Portfolio::class)
3476
                ->findBy($itemsCriteria, ['creationDate' => 'DESC']);
3477
        }
3478
3479
        return $items;
3480
    }
3481
3482
    /**
3483
     * @throws \Doctrine\ORM\ORMException
3484
     * @throws \Doctrine\ORM\OptimisticLockException
3485
     * @throws \Doctrine\ORM\TransactionRequiredException
3486
     */
3487
    private function createCommentForm(Portfolio $item): string
3488
    {
3489
        $formAction = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]);
3490
3491
        $templates = $this->em
3492
            ->getRepository(PortfolioComment::class)
3493
            ->findBy(
3494
                [
3495
                    'isTemplate' => true,
3496
                    'author' => $this->owner,
3497
                ]
3498
            );
3499
3500
        $form = new FormValidator('frm_comment', 'post', $formAction);
3501
        $form->addHeader(get_lang('AddNewComment'));
3502
        $form->addSelectFromCollection(
3503
            'template',
3504
            [
3505
                get_lang('Template'),
3506
                null,
3507
                '<span id="portfolio-spinner" class="fa fa-fw fa-spinner fa-spin" style="display: none;"
3508
                    aria-hidden="true" aria-label="'.get_lang('Loading').'"></span>',
3509
            ],
3510
            $templates,
3511
            [],
3512
            true,
3513
            'getExcerpt'
3514
        );
3515
        $form->addHtmlEditor('content', get_lang('Comments'), true, false, ['ToolbarSet' => 'Minimal']);
3516
        $form->addHidden('item', $item->getId());
3517
        $form->addHidden('parent', 0);
3518
        $form->applyFilter('content', 'trim');
3519
3520
        $this->addAttachmentsFieldToForm($form);
3521
3522
        $form->addButtonSave(get_lang('Save'));
3523
3524
        if ($form->validate()) {
3525
            $values = $form->exportValues();
3526
3527
            $parentComment = $this->em->find(PortfolioComment::class, $values['parent']);
3528
3529
            $comment = new PortfolioComment();
3530
            $comment
3531
                ->setAuthor($this->owner)
3532
                ->setParent($parentComment)
3533
                ->setContent($values['content'])
3534
                ->setDate(api_get_utc_datetime(null, false, true))
3535
                ->setItem($item);
3536
3537
            $this->em->persist($comment);
3538
            $this->em->flush();
3539
3540
            $this->processAttachments(
3541
                $form,
3542
                $comment->getAuthor(),
3543
                $comment->getId(),
3544
                PortfolioAttachment::TYPE_COMMENT
3545
            );
3546
3547
            $hook = HookPortfolioItemCommented::create();
3548
            $hook->setEventData(['comment' => $comment]);
3549
            $hook->notifyItemCommented();
3550
3551
            PortfolioNotifier::notifyTeachersAndAuthor($comment);
3552
3553
            Display::addFlash(
3554
                Display::return_message(get_lang('CommentAdded'), 'success')
3555
            );
3556
3557
            header("Location: $formAction");
3558
            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...
3559
        }
3560
3561
        $js = '<script>
3562
            $(function() {
3563
                $(\'#frm_comment_template\').on(\'change\', function () {
3564
                    $(\'#portfolio-spinner\').show();
3565
                
3566
                    $.getJSON(_p.web_ajax + \'portfolio.ajax.php?a=find_template_comment&comment=\' + this.value)
3567
                        .done(function(response) {
3568
                            CKEDITOR.instances.content.setData(response.content);
3569
                        })
3570
                        .fail(function () {
3571
                            CKEDITOR.instances.content.setData(\'\');
3572
                        })
3573
                        .always(function() {
3574
                          $(\'#portfolio-spinner\').hide();
3575
                        });
3576
                });
3577
            });
3578
        </script>';
3579
3580
        return $form->returnForm().$js;
3581
    }
3582
3583
    private function generateAttachmentList($post, bool $includeHeader = true): string
3584
    {
3585
        $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class);
3586
3587
        $postOwnerId = 0;
3588
3589
        if ($post instanceof Portfolio) {
3590
            $attachments = $attachmentsRepo->findFromItem($post);
3591
3592
            $postOwnerId = $post->getUser()->getId();
3593
        } elseif ($post instanceof PortfolioComment) {
3594
            $attachments = $attachmentsRepo->findFromComment($post);
3595
3596
            $postOwnerId = $post->getAuthor()->getId();
3597
        }
3598
3599
        if (empty($attachments)) {
3600
            return '';
3601
        }
3602
3603
        $currentUserId = api_get_user_id();
3604
3605
        $listItems = '<ul class="fa-ul">';
3606
3607
        $deleteIcon = Display::return_icon(
3608
            'delete.png',
3609
            get_lang('DeleteAttachment'),
3610
            ['style' => 'display: inline-block'],
3611
            ICON_SIZE_TINY
3612
        );
3613
        $deleteAttrs = ['class' => 'btn-portfolio-delete'];
3614
3615
        /** @var PortfolioAttachment $attachment */
3616
        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...
3617
            $downloadParams = http_build_query(['action' => 'download_attachment', 'file' => $attachment->getPath()]);
3618
            $deleteParams = http_build_query(['action' => 'delete_attachment', 'file' => $attachment->getPath()]);
3619
3620
            $listItems .= '<li>'
3621
                .'<span class="fa-li fa fa-paperclip" aria-hidden="true"></span>'
3622
                .Display::url(
3623
                    Security::remove_XSS($attachment->getFilename()),
3624
                    $this->baseUrl.$downloadParams
3625
                );
3626
3627
            if ($currentUserId === $postOwnerId) {
3628
                $listItems .= PHP_EOL.Display::url($deleteIcon, $this->baseUrl.$deleteParams, $deleteAttrs);
3629
            }
3630
3631
            if ($attachment->getComment()) {
3632
                $listItems .= '<p class="text-muted">'.Security::remove_XSS($attachment->getComment()).'</p>';
3633
            }
3634
3635
            $listItems .= '</li>';
3636
        }
3637
3638
        $listItems .= '</ul>';
3639
3640
        if ($includeHeader) {
3641
            $listItems = '<h1 class="h4">'.get_lang('FilesAttachment').'</h1>'
3642
                .$listItems;
3643
        }
3644
3645
        return $listItems;
3646
    }
3647
3648
    private function generateItemContent(Portfolio $item): string
3649
    {
3650
        $originId = $item->getOrigin();
3651
3652
        if (empty($originId)) {
3653
            return $item->getContent();
3654
        }
3655
3656
        $em = Database::getManager();
3657
3658
        $originContent = '';
3659
        $originContentFooter = '';
3660
3661
        if (Portfolio::TYPE_ITEM === $item->getOriginType()) {
3662
            $origin = $em->find(Portfolio::class, $item->getOrigin());
3663
3664
            if ($origin) {
3665
                $originContent = $origin->getContent();
3666
                $originContentFooter = vsprintf(
3667
                    get_lang('OriginallyPublishedAsXTitleByYUser'),
3668
                    [
3669
                        "<cite>{$origin->getTitle(true)}</cite>",
3670
                        $origin->getUser()->getCompleteName(),
3671
                    ]
3672
                );
3673
            }
3674
        } elseif (Portfolio::TYPE_COMMENT === $item->getOriginType()) {
3675
            $origin = $em->find(PortfolioComment::class, $item->getOrigin());
3676
3677
            if ($origin) {
3678
                $originContent = $origin->getContent();
3679
                $originContentFooter = vsprintf(
3680
                    get_lang('OriginallyCommentedByXUserInYItem'),
3681
                    [
3682
                        $origin->getAuthor()->getCompleteName(),
3683
                        "<cite>{$origin->getItem()->getTitle(true)}</cite>",
3684
                    ]
3685
                );
3686
            }
3687
        }
3688
3689
        if ($originContent) {
3690
            return "<figure>
3691
                    <blockquote>$originContent</blockquote>
3692
                    <figcaption style=\"margin-bottom: 10px;\">$originContentFooter</figcaption>
3693
                </figure>
3694
                <div class=\"clearfix\">".Security::remove_XSS($item->getContent()).'</div>'
3695
            ;
3696
        }
3697
3698
        return Security::remove_XSS($item->getContent());
3699
    }
3700
3701
    private function getItemsInHtmlFormatted(array $items): array
3702
    {
3703
        $itemsHtml = [];
3704
3705
        /** @var Portfolio $item */
3706
        foreach ($items as $item) {
3707
            $itemCourse = $item->getCourse();
3708
            $itemSession = $item->getSession();
3709
3710
            $creationDate = api_convert_and_format_date($item->getCreationDate());
3711
            $updateDate = api_convert_and_format_date($item->getUpdateDate());
3712
3713
            $metadata = '<ul class="list-unstyled text-muted">';
3714
3715
            if ($itemSession) {
3716
                $metadata .= '<li>'.get_lang('Course').': '.$itemSession->getName().' ('
3717
                    .$itemCourse->getTitle().') </li>';
3718
            } elseif ($itemCourse) {
3719
                $metadata .= '<li>'.get_lang('Course').': '.$itemCourse->getTitle().'</li>';
3720
            }
3721
3722
            $metadata .= '<li>'.sprintf(get_lang('CreationDateXDate'), $creationDate).'</li>';
3723
3724
            if ($itemCourse) {
3725
                $propertyInfo = api_get_item_property_info(
3726
                    $itemCourse->getId(),
3727
                    TOOL_PORTFOLIO,
3728
                    $item->getId(),
3729
                    $itemSession ? $itemSession->getId() : 0
3730
                );
3731
3732
                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...
3733
                    $metadata .= '<li>'
3734
                        .sprintf(
3735
                            get_lang('UpdatedOnDateXByUserY'),
3736
                            api_convert_and_format_date($propertyInfo['lastedit_date'], DATE_TIME_FORMAT_LONG),
3737
                            api_get_user_entity($propertyInfo['lastedit_user_id'])->getCompleteName()
3738
                        )
3739
                        .'</li>';
3740
                }
3741
            } else {
3742
                $metadata .= '<li>'.sprintf(get_lang('UpdateDateXDate'), $updateDate).'</li>';
3743
            }
3744
3745
            if ($item->getCategory()) {
3746
                $metadata .= '<li>'.sprintf(get_lang('CategoryXName'), $item->getCategory()->getTitle()).'</li>';
3747
            }
3748
3749
            $metadata .= '</ul>';
3750
3751
            $itemContent = $this->generateItemContent($item);
3752
3753
            $itemsHtml[] = Display::panel($itemContent, Security::remove_XSS($item->getTitle()), '', 'info', $metadata);
3754
        }
3755
3756
        return $itemsHtml;
3757
    }
3758
3759
    private function getCommentsInHtmlFormatted(array $comments): array
3760
    {
3761
        $commentsHtml = [];
3762
3763
        /** @var PortfolioComment $comment */
3764
        foreach ($comments as $comment) {
3765
            $item = $comment->getItem();
3766
            $date = api_convert_and_format_date($comment->getDate());
3767
3768
            $metadata = '<ul class="list-unstyled text-muted">';
3769
            $metadata .= '<li>'.sprintf(get_lang('DateXDate'), $date).'</li>';
3770
            $metadata .= '<li>'.sprintf(get_lang('PortfolioItemTitleXName'), Security::remove_XSS($item->getTitle()))
3771
                .'</li>';
3772
            $metadata .= '</ul>';
3773
3774
            $commentsHtml[] = Display::panel(
3775
                Security::remove_XSS($comment->getContent()),
3776
                '',
3777
                '',
3778
                'default',
3779
                $metadata
3780
            );
3781
        }
3782
3783
        return $commentsHtml;
3784
    }
3785
3786
    private function fixImagesSourcesToHtml(string $htmlContent): string
3787
    {
3788
        $doc = new DOMDocument();
3789
        @$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

3789
        /** @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...
3790
3791
        $elements = $doc->getElementsByTagName('img');
3792
3793
        if (empty($elements->length)) {
3794
            return $htmlContent;
3795
        }
3796
3797
        $webCoursePath = api_get_path(WEB_COURSE_PATH);
3798
        $webUploadPath = api_get_path(WEB_UPLOAD_PATH);
3799
3800
        /** @var \DOMElement $element */
3801
        foreach ($elements as $element) {
3802
            $src = trim($element->getAttribute('src'));
3803
3804
            if (strpos($src, 'http') === 0) {
3805
                continue;
3806
            }
3807
3808
            if (strpos($src, '/app/upload/') === 0) {
3809
                $element->setAttribute(
3810
                    'src',
3811
                    preg_replace('/\/app/upload\//', $webUploadPath, $src, 1)
3812
                );
3813
3814
                continue;
3815
            }
3816
3817
            if (strpos($src, '/courses/') === 0) {
3818
                $element->setAttribute(
3819
                    'src',
3820
                    preg_replace('/\/courses\//', $webCoursePath, $src, 1)
3821
                );
3822
3823
                continue;
3824
            }
3825
        }
3826
3827
        return $doc->saveHTML();
3828
    }
3829
3830
    private function formatZipIndexFile(HTML_Table $tblItems, HTML_Table $tblComments): string
3831
    {
3832
        $htmlContent = Display::page_header($this->owner->getCompleteNameWithUsername());
3833
        $htmlContent .= Display::page_subheader2(get_lang('PortfolioItems'));
3834
3835
        $htmlContent .= $tblItems->getRowCount() > 0
3836
            ? $tblItems->toHtml()
3837
            : Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
3838
3839
        $htmlContent .= Display::page_subheader2(get_lang('PortfolioCommentsMade'));
3840
3841
        $htmlContent .= $tblComments->getRowCount() > 0
3842
            ? $tblComments->toHtml()
3843
            : Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
3844
3845
        $webAssetsPath = api_get_path(WEB_PUBLIC_PATH).'assets/';
3846
3847
        $doc = new DOMDocument();
3848
        @$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

3848
        /** @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...
3849
3850
        $stylesheet1 = $doc->createElement('link');
3851
        $stylesheet1->setAttribute('rel', 'stylesheet');
3852
        $stylesheet1->setAttribute('href', $webAssetsPath.'bootstrap/dist/css/bootstrap.min.css');
3853
        $stylesheet2 = $doc->createElement('link');
3854
        $stylesheet2->setAttribute('rel', 'stylesheet');
3855
        $stylesheet2->setAttribute('href', $webAssetsPath.'fontawesome/css/font-awesome.min.css');
3856
        $stylesheet3 = $doc->createElement('link');
3857
        $stylesheet3->setAttribute('rel', 'stylesheet');
3858
        $stylesheet3->setAttribute('href', ChamiloApi::getEditorDocStylePath());
3859
3860
        $head = $doc->createElement('head');
3861
        $head->appendChild($stylesheet1);
3862
        $head->appendChild($stylesheet2);
3863
        $head->appendChild($stylesheet3);
3864
3865
        $doc->documentElement->insertBefore(
3866
            $head,
3867
            $doc->getElementsByTagName('body')->item(0)
3868
        );
3869
3870
        return $doc->saveHTML();
3871
    }
3872
3873
    /**
3874
     * It parsers a title for a variable in lang.
3875
     *
3876
     * @param $defaultDisplayText
3877
     *
3878
     * @return string
3879
     */
3880
    private function getLanguageVariable($defaultDisplayText)
3881
    {
3882
        $variableLanguage = api_replace_dangerous_char(strtolower($defaultDisplayText));
3883
        $variableLanguage = preg_replace('/[^A-Za-z0-9\_]/', '', $variableLanguage); // Removes special chars except underscore.
3884
        if (is_numeric($variableLanguage[0])) {
3885
            $variableLanguage = '_'.$variableLanguage;
3886
        }
3887
        $variableLanguage = api_underscore_to_camel_case($variableLanguage);
3888
3889
        return $variableLanguage;
3890
    }
3891
3892
    /**
3893
     * It translates the text as parameter.
3894
     *
3895
     * @param $defaultDisplayText
3896
     *
3897
     * @return mixed
3898
     */
3899
    private function translateDisplayName($defaultDisplayText)
3900
    {
3901
        $variableLanguage = $this->getLanguageVariable($defaultDisplayText);
3902
3903
        return isset($GLOBALS[$variableLanguage]) ? $GLOBALS[$variableLanguage] : $defaultDisplayText;
3904
    }
3905
3906
    private function getCommentsForIndex(FormValidator $frmFilterList = null): array
3907
    {
3908
        if (null === $frmFilterList) {
3909
            return [];
3910
        }
3911
3912
        if (!$frmFilterList->validate()) {
3913
            return [];
3914
        }
3915
3916
        $values = $frmFilterList->exportValues();
3917
3918
        if (empty($values['date']) && empty($values['text'])) {
3919
            return [];
3920
        }
3921
3922
        $queryBuilder = $this->em->createQueryBuilder()
3923
            ->select('c')
3924
            ->from(PortfolioComment::class, 'c')
3925
        ;
3926
3927
        if (!empty($values['date'])) {
3928
            $queryBuilder
3929
                ->andWhere('c.date >= :date')
3930
                ->setParameter(':date', api_get_utc_datetime($values['date'], false, true))
3931
            ;
3932
        }
3933
3934
        if (!empty($values['text'])) {
3935
            $queryBuilder
3936
                ->andWhere('c.content LIKE :text')
3937
                ->setParameter('text', '%'.$values['text'].'%')
3938
            ;
3939
        }
3940
3941
        $queryBuilder->orderBy('c.date', 'DESC');
3942
3943
        return $queryBuilder->getQuery()->getResult();
3944
    }
3945
3946
    private function getLabelForCommentDate(PortfolioComment $comment): string
3947
    {
3948
        $item = $comment->getItem();
3949
        $commmentCourse = $item->getCourse();
3950
        $commmentSession = $item->getSession();
3951
3952
        $dateLabel = Display::dateToStringAgoAndLongDate($comment->getDate()).PHP_EOL;
3953
3954
        if ($commmentCourse) {
0 ignored issues
show
introduced by
$commmentCourse is of type Chamilo\CoreBundle\Entity\Course, thus it always evaluated to true.
Loading history...
3955
            $propertyInfo = api_get_item_property_info(
3956
                $commmentCourse->getId(),
3957
                TOOL_PORTFOLIO,
3958
                $comment->getId(),
3959
                $commmentSession ? $commmentSession->getId() : 0
0 ignored issues
show
introduced by
$commmentSession is of type Chamilo\CoreBundle\Entity\Session, thus it always evaluated to true.
Loading history...
3960
            );
3961
3962
            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...
3963
                $dateLabel .= '|'.PHP_EOL
3964
                    .sprintf(
3965
                        get_lang('UpdatedDateX'),
3966
                        Display::dateToStringAgoAndLongDate($propertyInfo['lastedit_date'])
3967
                    );
3968
            }
3969
        }
3970
3971
        return $dateLabel;
3972
    }
3973
}
3974