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

PortfolioController::editCategory()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 84
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 53
c 0
b 0
f 0
nc 12
nop 1
dl 0
loc 84
rs 8.7143

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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

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

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