Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions install/empty_data.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ public function getEmptyData(): array
'maintenance_mode' => '0',
'maintenance_text' => '',
'attach_ticket_documents_to_mail' => '0',
'attach_documents_to_notifications_for_anonymous' => '0',
'backcreated' => '0',
'task_state' => '1',
'palette' => 'auror',
Expand Down
71 changes: 71 additions & 0 deletions install/migrations/update_10.0.24_to_10.0.25.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2026 Teclib' and contributors.
* @licence https://www.gnu.org/licenses/gpl-3.0.html
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/

/**
* Update from 10.0.24 to 10.0.25
*
* @return bool for success (will die for most error)
**/
function update10024to10025()
{
/**
* @var \DBmysql $DB
* @var \Migration $migration
*/
global $DB, $migration;

$updateresult = true;
$ADDTODISPLAYPREF = [];
$DELFROMDISPLAYPREF = [];
$update_dir = __DIR__ . '/update_10.0.24_to_10.0.25/';

//TRANS: %s is the number of new version
$migration->displayTitle(sprintf(__('Update to %s'), '10.0.25'));
$migration->setVersion('10.0.25');

$update_scripts = scandir($update_dir);
foreach ($update_scripts as $update_script) {
if (preg_match('/\.php$/', $update_script) !== 1) {
continue;
}
require $update_dir . $update_script;
}

// ************ Keep it at the end **************
$migration->updateDisplayPrefs($ADDTODISPLAYPREF, $DELFROMDISPLAYPREF);

$migration->executeMigration();

return $updateresult;
}
40 changes: 40 additions & 0 deletions install/migrations/update_10.0.24_to_10.0.25/config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2026 Teclib' and contributors.
* @licence https://www.gnu.org/licenses/gpl-3.0.html
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/

/**
* @var \Migration $migration
*/

// Add configuration option to control document attachment for anonymous users in notifications
$migration->addConfig(['attach_documents_to_notifications_for_anonymous' => 0]);
54 changes: 46 additions & 8 deletions phpunit/functional/TicketTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8555,7 +8555,7 @@ public function testGetAssociatedDocuments(): void
$this->assertNotContains($doc3->getID(), $found_docs);

// Anonymous user can't see documents linked to private followups
$doc_crit = $ticket->getAssociatedDocumentsCriteria(false, new User());
$doc_crit = $ticket->getAssociatedDocumentsCriteria(false, null, true);
$doc_crit[] = [
'timeline_position' => ['>', CommonITILObject::NO_TIMELINE],
];
Expand Down Expand Up @@ -8664,6 +8664,43 @@ public static function associatedDocumentsWithoutSessionProvider(): iterable
'test_user' => 'post-only',
'expected' => true,
];

// Tests for anonymous user (no GLPI account)
yield [
'parent_itil_itemtype' => Ticket::class,
'timeline_item_type' => \ITILFollowup::class,
'is_private' => false,
'test_user' => null, // anonymous
'expected' => true,
];
yield [
'parent_itil_itemtype' => Ticket::class,
'timeline_item_type' => \ITILFollowup::class,
'is_private' => true,
'test_user' => null, // anonymous
'expected' => false,
];
yield [
'parent_itil_itemtype' => Ticket::class,
'timeline_item_type' => \TicketTask::class,
'is_private' => false,
'test_user' => null, // anonymous
'expected' => true,
];
yield [
'parent_itil_itemtype' => Ticket::class,
'timeline_item_type' => \TicketTask::class,
'is_private' => true,
'test_user' => null, // anonymous
'expected' => false,
];
yield [
'parent_itil_itemtype' => Ticket::class,
'timeline_item_type' => \ITILSolution::class,
'is_private' => false,
'test_user' => null, // anonymous
'expected' => true,
];
}

/**
Expand All @@ -8677,21 +8714,22 @@ public function testGetAssociatedDocumentsWithoutActiveSession(
string $parent_itil_itemtype,
string $timeline_item_type,
bool $is_private,
string $test_user,
?string $test_user,
bool $expected
): void {
global $DB;

$this->login();

// Get the test user
$user = getItemByTypeName(User::class, $test_user, false);
// Get the test user (or anonymous)
$user = $test_user !== null ? getItemByTypeName(User::class, $test_user, false) : null;
$is_anonymous = ($test_user === null);

$parent_item = $this->createItem($parent_itil_itemtype, [
'name' => 'ITIL Object test',
'content' => 'test',
'entities_id' => $this->getTestRootEntity(true),
'_users_id_requester' => $user->getID(),
'_users_id_requester' => $is_anonymous ? 0 : $user->getID(),
]);

// Create a document linked directly to the parent item (ticket/change/problem)
Expand Down Expand Up @@ -8743,7 +8781,7 @@ public function testGetAssociatedDocumentsWithoutActiveSession(
$timeline_item = $this->createItem($timeline_item_type, [
$fk_field => $parent_item->getID(),
'comment_submission' => 'Validation request with document',
'users_id_validate' => $user->getID(),
'users_id_validate' => $is_anonymous ? Session::getLoginUserID() : $user->getID(),
]);
$doc_timeline = $this->createItem(\Document::class, [
'name' => 'Doc: linked to ticket validation',
Expand All @@ -8759,7 +8797,7 @@ public function testGetAssociatedDocumentsWithoutActiveSession(
]);

// First verify with active session
$doc_crit = $parent_item->getAssociatedDocumentsCriteria(false, $user);
$doc_crit = $parent_item->getAssociatedDocumentsCriteria(false, $is_anonymous ? null : $user, $is_anonymous);
$doc_items_iterator = $DB->request([
'SELECT' => ['documents_id'],
'FROM' => \Document_Item::getTable(),
Expand Down Expand Up @@ -8797,7 +8835,7 @@ public function testGetAssociatedDocumentsWithoutActiveSession(
$parent_item->getFromDB($parent_item->getID());

// Test that documents visibility is consistent without session
$doc_crit = $parent_item->getAssociatedDocumentsCriteria(false, $user);
$doc_crit = $parent_item->getAssociatedDocumentsCriteria(false, $is_anonymous ? null : $user, $is_anonymous);
$doc_items_iterator = $DB->request([
'SELECT' => ['documents_id'],
'FROM' => \Document_Item::getTable(),
Expand Down
64 changes: 43 additions & 21 deletions src/CommonITILObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -7956,18 +7956,24 @@ public function getForbiddenSingleMassiveActions()
/**
* Returns criteria that can be used to get documents related to current instance.
*
* @param bool $bypass_rights Whether to bypass rights checks (default: false)
* @FIXME uncomment @param User|null $user User for rights checking (default: null = current session rights)
* @param bool $bypass_rights Whether to bypass rights checks (default: false)
* @FIXME uncomment @param User|null $user User for rights checking (default: null = current session rights)
* @FIXME uncomment @param bool $for_anonymous_user Whether the user is an anonymous requester (default: false)
*
* @return array
*/
public function getAssociatedDocumentsCriteria($bypass_rights = false/*, ?User $user = null*/): array
public function getAssociatedDocumentsCriteria($bypass_rights = false/*, ?User $user = null, bool $for_anonymous_user = false*/): array
{
$user = null;
if (func_num_args() == 2) {
if (func_num_args() >= 2) {
$user = func_get_arg(1);
}

$for_anonymous_user = false;
if (func_num_args() >= 3) {
$for_anonymous_user = (bool) func_get_arg(2);
}

$user_id = $user ? $user->getID() : Session::getLoginUserID();

$can_view_itilobject = [
Expand Down Expand Up @@ -8000,24 +8006,31 @@ public function getAssociatedDocumentsCriteria($bypass_rights = false/*, ?User $
$bypass_rights ||
ITILFollowup::canView() ||
(
!$for_anonymous_user &&
$user !== null &&
$user->hasRightsOr(static::$rightname, $can_view_itilobject[$this->getType()], $this->fields['entities_id']) &&
$user->hasRight(ITILFollowup::$rightname, ITILFollowup::SEEPUBLIC, $this->fields['entities_id'])
)
) ||
$for_anonymous_user // anonymous user
) {
$fup_crits = [
ITILFollowup::getTableField('itemtype') => $this->getType(),
ITILFollowup::getTableField('items_id') => $this->getID(),
];
if (!$bypass_rights) {
$can_seeprivate = ($user === null)
? Session::haveRight(ITILFollowup::$rightname, ITILFollowup::SEEPRIVATE)
: $user->hasRight(ITILFollowup::$rightname, ITILFollowup::SEEPRIVATE, $this->fields['entities_id']);
if ($for_anonymous_user) {
// Anonymous user: can only see public followups
$fup_crits[] = ['is_private' => 0];
} else {
$can_seeprivate = ($user === null)
? Session::haveRight(ITILFollowup::$rightname, ITILFollowup::SEEPRIVATE)
: $user->hasRight(ITILFollowup::$rightname, ITILFollowup::SEEPRIVATE, $this->fields['entities_id']);

if (!$can_seeprivate) {
$fup_crits[] = [
'OR' => ['is_private' => 0, 'users_id' => $user_id],
];
if (!$can_seeprivate) {
$fup_crits[] = [
'OR' => ['is_private' => 0, 'users_id' => $user_id],
];
}
}
}

Expand All @@ -8041,9 +8054,11 @@ public function getAssociatedDocumentsCriteria($bypass_rights = false/*, ?User $
$bypass_rights ||
ITILSolution::canView() ||
(
!$for_anonymous_user &&
$user !== null &&
$user->hasRightsOr(static::$rightname, $can_view_itilobject[$this->getType()], $this->fields['entities_id'])
)
) ||
$for_anonymous_user // anonymous user
) {
// Run the subquery separately. It's better for huge databases
$iterator_tmp = $DB->request([
Expand Down Expand Up @@ -8107,23 +8122,30 @@ class_exists($validation_class) &&
$bypass_rights ||
$task_class::canView() ||
(
!$for_anonymous_user &&
$user !== null &&
$user->hasRightsOr(static::$rightname, $can_view_itilobject[$this->getType()], $this->fields['entities_id']) &&
$user->hasRight($task_class::$rightname, $task_class::SEEPUBLIC, $this->fields['entities_id'])
)
) ||
$for_anonymous_user // anonymous user
) {
$tasks_crit = [
$this->getForeignKeyField() => $this->getID(),
];
if (!$bypass_rights) {
$can_seeprivate = ($user === null)
? Session::haveRight($task_class::$rightname, CommonITILTask::SEEPRIVATE)
: $user->hasRight($task_class::$rightname, CommonITILTask::SEEPRIVATE, $this->fields['entities_id']);
if ($for_anonymous_user) {
// Anonymous user: can only see public tasks
$tasks_crit[] = ['is_private' => 0];
} else {
$can_seeprivate = ($user === null)
? Session::haveRight($task_class::$rightname, CommonITILTask::SEEPRIVATE)
: $user->hasRight($task_class::$rightname, CommonITILTask::SEEPRIVATE, $this->fields['entities_id']);

if (!$can_seeprivate) {
$tasks_crit[] = [
'OR' => ['is_private' => 0, 'users_id' => $user_id],
];
if (!$can_seeprivate) {
$tasks_crit[] = [
'OR' => ['is_private' => 0, 'users_id' => $user_id],
];
}
}
}
// Run the subquery separately. It's better for huge databases
Expand Down
14 changes: 9 additions & 5 deletions src/NotificationEventMailing.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ public static function send(array $data)

$documents_ids = [];
$documents_to_attach = [];
$user = new User();
$is_anonymous_requester = !$user->getFromDBbyEmail($current->fields['recipient']);
if ($is_html || $CFG_GLPI['attach_ticket_documents_to_mail']) {
// Retieve document list if mail is in HTML format (for inline images)
// or if documents are attached to mail.
Expand All @@ -201,9 +203,7 @@ public static function send(array $data)
'itemtype' => $current->fields['itemtype'],
];
if ($item instanceof CommonITILObject) {
$user = new User();
$user->getFromDBbyEmail($current->fields['recipient']);
$doc_crit = $item->getAssociatedDocumentsCriteria(false, $user);
$doc_crit = $item->getAssociatedDocumentsCriteria(false, $is_anonymous_requester ? null : $user, $is_anonymous_requester);
if ($is_html) {
// Remove documents having "NO_TIMELINE" position if mail is HTML, as
// these documents corresponds to inlined images.
Expand All @@ -229,7 +229,9 @@ public static function send(array $data)
$mmail->isHTML($is_html);
if (!$is_html) {
$mmail->Body = GLPIMailer::normalizeBreaks($current->fields['body_text']);
$documents_to_attach = $documents_ids; // Attach all documents
if (!$is_anonymous_requester || $CFG_GLPI['attach_documents_to_notifications_for_anonymous']) {
$documents_to_attach = $documents_ids; // Attach all documents
}
} else {
$mmail->Body = '';
$inline_docs = [];
Expand Down Expand Up @@ -270,7 +272,9 @@ public static function send(array $data)
}
} else {
// Attach only documents that are not inlined images
$documents_to_attach[] = $document_id;
if (!$is_anonymous_requester || $CFG_GLPI['attach_documents_to_notifications_for_anonymous']) {
$documents_to_attach[] = $document_id;
}
}
}

Expand Down
Loading
Loading