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

PortfolioController::deleteCategory()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 1
dl 0
loc 15
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Entity\Course as CourseEntity;
7
use Chamilo\CoreBundle\Entity\ExtraField as ExtraFieldEntity;
8
use Chamilo\CoreBundle\Entity\ExtraFieldRelTag;
9
use Chamilo\CoreBundle\Entity\Portfolio;
10
use Chamilo\CoreBundle\Entity\PortfolioAttachment;
11
use Chamilo\CoreBundle\Entity\PortfolioCategory;
12
use Chamilo\CoreBundle\Entity\PortfolioComment;
13
use Chamilo\CoreBundle\Entity\Tag;
14
use Chamilo\UserBundle\Entity\User;
15
use Doctrine\ORM\Query\Expr\Join;
16
use Symfony\Component\Filesystem\Filesystem;
17
use Symfony\Component\HttpFoundation\Request as HttpRequest;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, HttpRequest. Consider defining an alias.

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

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

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

3322
        /** @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...
3323
3324
        $elements = $doc->getElementsByTagName('img');
3325
3326
        if (empty($elements->length)) {
3327
            return $htmlContent;
3328
        }
3329
3330
        $webCoursePath = api_get_path(WEB_COURSE_PATH);
3331
        $webUploadPath = api_get_path(WEB_UPLOAD_PATH);
3332
3333
        /** @var \DOMElement $element */
3334
        foreach ($elements as $element) {
3335
            $src = trim($element->getAttribute('src'));
3336
3337
            if (strpos($src, 'http') === 0) {
3338
                continue;
3339
            }
3340
3341
            if (strpos($src, '/app/upload/') === 0) {
3342
                $element->setAttribute(
3343
                    'src',
3344
                    preg_replace('/\/app/upload\//', $webUploadPath, $src, 1)
3345
                );
3346
3347
                continue;
3348
            }
3349
3350
            if (strpos($src, '/courses/') === 0) {
3351
                $element->setAttribute(
3352
                    'src',
3353
                    preg_replace('/\/courses\//', $webCoursePath, $src, 1)
3354
                );
3355
3356
                continue;
3357
            }
3358
        }
3359
3360
        return $doc->saveHTML();
3361
    }
3362
3363
    private function formatZipIndexFile(HTML_Table $tblItems, HTML_Table $tblComments): string
3364
    {
3365
        $htmlContent = Display::page_header($this->owner->getCompleteNameWithUsername());
3366
        $htmlContent .= Display::page_subheader2(get_lang('PortfolioItems'));
3367
3368
        $htmlContent .= $tblItems->getRowCount() > 0
3369
            ? $tblItems->toHtml()
3370
            : Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
3371
3372
        $htmlContent .= Display::page_subheader2(get_lang('PortfolioCommentsMade'));
3373
3374
        $htmlContent .= $tblComments->getRowCount() > 0
3375
            ? $tblComments->toHtml()
3376
            : Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
3377
3378
        $webAssetsPath = api_get_path(WEB_PUBLIC_PATH).'assets/';
3379
3380
        $doc = new DOMDocument();
3381
        @$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

3381
        /** @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...
3382
3383
        $stylesheet1 = $doc->createElement('link');
3384
        $stylesheet1->setAttribute('rel', 'stylesheet');
3385
        $stylesheet1->setAttribute('href', $webAssetsPath.'bootstrap/dist/css/bootstrap.min.css');
3386
        $stylesheet2 = $doc->createElement('link');
3387
        $stylesheet2->setAttribute('rel', 'stylesheet');
3388
        $stylesheet2->setAttribute('href', $webAssetsPath.'fontawesome/css/font-awesome.min.css');
3389
        $stylesheet3 = $doc->createElement('link');
3390
        $stylesheet3->setAttribute('rel', 'stylesheet');
3391
        $stylesheet3->setAttribute('href', ChamiloApi::getEditorDocStylePath());
3392
3393
        $head = $doc->createElement('head');
3394
        $head->appendChild($stylesheet1);
3395
        $head->appendChild($stylesheet2);
3396
        $head->appendChild($stylesheet3);
3397
3398
        $doc->documentElement->insertBefore(
3399
            $head,
3400
            $doc->getElementsByTagName('body')->item(0)
3401
        );
3402
3403
        return $doc->saveHTML();
3404
    }
3405
3406
    /**
3407
     * It parsers a title for a variable in lang.
3408
     *
3409
     * @param $defaultDisplayText
3410
     *
3411
     * @return string
3412
     */
3413
    private function getLanguageVariable($defaultDisplayText)
3414
    {
3415
        $variableLanguage = api_replace_dangerous_char(strtolower($defaultDisplayText));
3416
        $variableLanguage = preg_replace('/[^A-Za-z0-9\_]/', '', $variableLanguage); // Removes special chars except underscore.
3417
        if (is_numeric($variableLanguage[0])) {
3418
            $variableLanguage = '_'.$variableLanguage;
3419
        }
3420
        $variableLanguage = api_underscore_to_camel_case($variableLanguage);
3421
3422
        return $variableLanguage;
3423
    }
3424
3425
    /**
3426
     * It translates the text as parameter.
3427
     *
3428
     * @param $defaultDisplayText
3429
     *
3430
     * @return mixed
3431
     */
3432
    private function translateDisplayName($defaultDisplayText)
3433
    {
3434
        $variableLanguage = $this->getLanguageVariable($defaultDisplayText);
3435
3436
        return isset($GLOBALS[$variableLanguage]) ? $GLOBALS[$variableLanguage] : $defaultDisplayText;
3437
    }
3438
}
3439