diff --git a/ajax/visibility.php b/ajax/visibility.php index 789a2650c5f..a0de7171494 100644 --- a/ajax/visibility.php +++ b/ajax/visibility.php @@ -45,7 +45,6 @@ if ( isset($_POST['type']) && !empty($_POST['type']) - && isset($_POST['right']) ) { $display = false; $rand = mt_rand(); @@ -62,9 +61,16 @@ switch ($_POST['type']) { case 'User': $params = [ - 'right' => isset($_POST['allusers']) ? 'all' : $_POST['right'], + 'right' => 'all', 'name' => $prefix . 'users_id' . $suffix, ]; + if (isset($_POST['right']) && !isset($_POST['allusers'])) { + $params['right'] = $_POST['right']; + } + if (isset($_POST['entity']) && $_POST['entity'] >= 0) { + $params['entity'] = $_POST['entity']; + $params['entity_sons'] = $_POST['is_recursive'] ?? false; + } User::dropdown($params); $display = true; break; @@ -82,7 +88,6 @@ 'prefix' => $_POST['prefix'], ], ]; - Group::dropdown($params); echo ""; $display = true; @@ -101,27 +106,26 @@ break; case 'Profile': - $checkright = (READ | CREATE | UPDATE | PURGE); - $righttocheck = $_POST['right']; - if ($_POST['right'] == 'faq') { - $righttocheck = 'knowbase'; - $checkright = KnowbaseItem::READFAQ; + $righttocheck = $_POST['right'] ?? null; + if ($righttocheck) { + $checkright = (READ | CREATE | UPDATE | PURGE); + if ($_POST['right'] == 'faq') { + $righttocheck = 'knowbase'; + $checkright = KnowbaseItem::READFAQ; + } + $params['condition'] = [ + 'glpi_profilerights.name' => $righttocheck, + 'glpi_profilerights.rights' => ['&', $checkright], + ]; } - $params = [ - 'rand' => $rand, - 'name' => $prefix . 'profiles_id' . $suffix, - 'condition' => [ - 'glpi_profilerights.name' => $righttocheck, - 'glpi_profilerights.rights' => ['&', $checkright], - ], - ]; - $params['toupdate'] = ['value_fieldname' - => 'value', - 'to_update' => "subvisibility$rand", - 'url' => $CFG_GLPI["root_doc"] . "/ajax/subvisibility.php", - 'moreparams' => ['items_id' => '__VALUE__', - 'type' => $_POST['type'], - 'prefix' => $_POST['prefix'], + $params['toupdate'] = [ + 'value_fieldname' => 'value', + 'to_update' => "subvisibility$rand", + 'url' => $CFG_GLPI["root_doc"] . "/ajax/subvisibility.php", + 'moreparams' => [ + 'items_id' => '__VALUE__', + 'type' => $_POST['type'], + 'prefix' => $_POST['prefix'], ], ]; diff --git a/front/savedsearch.php b/front/savedsearch.php index 72971a51082..bae7ac3eb7d 100644 --- a/front/savedsearch.php +++ b/front/savedsearch.php @@ -33,6 +33,8 @@ * --------------------------------------------------------------------- */ +use Glpi\Exception\Http\AccessDeniedHttpException; + require_once(__DIR__ . '/_check_webserver_config.php'); if (Session::getCurrentInterface() == "helpdesk") { @@ -47,8 +49,13 @@ isset($_GET['action']) && $_GET["action"] == "load" && isset($_GET["id"]) && ($_GET["id"] > 0) ) { - $savedsearch->check($_GET["id"], READ); - $savedsearch->load($_GET["id"]); + $savedsearch->getFromDB($_GET['id']); + if ($savedsearch->canViewItem()) { + $savedsearch->load($_GET["id"]); + } else { + $info = "User can not access the SavedSearch " . $_GET['id']; + throw new AccessDeniedHttpException($info); + } return; } diff --git a/inc/relation.constant.php b/inc/relation.constant.php index f48160abd12..7bc199f5f38 100644 --- a/inc/relation.constant.php +++ b/inc/relation.constant.php @@ -588,6 +588,7 @@ '_glpi_entities_knowbaseitems' => 'entities_id', '_glpi_entities_reminders' => 'entities_id', '_glpi_entities_rssfeeds' => 'entities_id', + '_glpi_entities_savedsearches' => 'entities_id', 'glpi_fieldblacklists' => 'entities_id', 'glpi_fieldunicities' => 'entities_id', 'glpi_forms_forms' => 'entities_id', @@ -597,6 +598,7 @@ 'glpi_groups_knowbaseitems' => 'entities_id', 'glpi_groups_reminders' => 'entities_id', 'glpi_groups_rssfeeds' => 'entities_id', + 'glpi_groups_savedsearches' => 'entities_id', 'glpi_holidays' => 'entities_id', 'glpi_imageformats' => 'entities_id', 'glpi_imageresolutions' => 'entities_id', @@ -740,6 +742,7 @@ '_glpi_groups_problems' => 'groups_id', '_glpi_groups_reminders' => 'groups_id', '_glpi_groups_rssfeeds' => 'groups_id', + '_glpi_groups_savedsearches' => 'groups_id', '_glpi_groups_tickets' => 'groups_id', '_glpi_groups_users' => 'groups_id', 'glpi_itilcategories' => 'groups_id', @@ -1275,8 +1278,11 @@ ], 'glpi_savedsearches' => [ - '_glpi_savedsearches_alerts' => 'savedsearches_id', - '_glpi_savedsearches_users' => 'savedsearches_id', + '_glpi_entities_savedsearches' => 'savedsearches_id', + '_glpi_groups_savedsearches' => 'savedsearches_id', + '_glpi_savedsearches_alerts' => 'savedsearches_id', + '_glpi_savedsearches_users' => 'savedsearches_id', + '_glpi_savedsearches_usertargets' => 'savedsearches_id', ], 'glpi_slalevels' => [ @@ -1638,6 +1644,7 @@ '_glpi_rssfeeds_users' => 'users_id', '_glpi_savedsearches' => 'users_id', '_glpi_savedsearches_users' => 'users_id', + '_glpi_savedsearches_usertargets' => 'users_id', 'glpi_softwarelicenses' => [ 'users_id_tech', 'users_id', diff --git a/install/migrations/update_10.0.x_to_11.0.0/entity_savedsearch.php b/install/migrations/update_10.0.x_to_11.0.0/entity_savedsearch.php new file mode 100644 index 00000000000..fae0e277efc --- /dev/null +++ b/install/migrations/update_10.0.x_to_11.0.0/entity_savedsearch.php @@ -0,0 +1,57 @@ +. + * + * --------------------------------------------------------------------- + */ + +/** + * @var \DBmysql $DB + * @var \Migration $migration + */ + +$default_charset = DBConnection::getDefaultCharset(); +$default_collation = DBConnection::getDefaultCollation(); +$default_key_sign = DBConnection::getDefaultPrimaryKeySignOption(); + +if (!$DB->tableExists('glpi_entities_savedsearches')) { + $query = "CREATE TABLE `glpi_entities_savedsearches` ( + `id` int {$default_key_sign} NOT NULL AUTO_INCREMENT, + `savedsearches_id` int {$default_key_sign} NOT NULL DEFAULT '0', + `entities_id` int {$default_key_sign} NOT NULL DEFAULT '0', + `is_recursive` tinyint NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `savedsearches_id` (`savedsearches_id`), + KEY `entities_id` (`entities_id`), + KEY `is_recursive` (`is_recursive`) + ) ENGINE = InnoDB ROW_FORMAT = DYNAMIC DEFAULT CHARSET = {$default_charset} COLLATE = {$default_collation};"; + $DB->doQuery($query); +} diff --git a/install/migrations/update_10.0.x_to_11.0.0/group_savedsearch.php b/install/migrations/update_10.0.x_to_11.0.0/group_savedsearch.php new file mode 100644 index 00000000000..33e9210ae64 --- /dev/null +++ b/install/migrations/update_10.0.x_to_11.0.0/group_savedsearch.php @@ -0,0 +1,60 @@ +. + * + * --------------------------------------------------------------------- + */ + +/** + * @var \DBmysql $DB + * @var \Migration $migration + */ + +$default_charset = DBConnection::getDefaultCharset(); +$default_collation = DBConnection::getDefaultCollation(); +$default_key_sign = DBConnection::getDefaultPrimaryKeySignOption(); + +if (!$DB->tableExists('glpi_groups_savedsearches')) { + $query = "CREATE TABLE `glpi_groups_savedsearches` ( + `id` int {$default_key_sign} NOT NULL AUTO_INCREMENT, + `savedsearches_id` int {$default_key_sign} NOT NULL DEFAULT '0', + `groups_id` int {$default_key_sign} NOT NULL DEFAULT '0', + `entities_id` int {$default_key_sign} DEFAULT NULL, + `is_recursive` tinyint NOT NULL DEFAULT '0', + `no_entity_restriction` tinyint NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `savedsearches_id` (`savedsearches_id`), + KEY `groups_id` (`groups_id`), + KEY `entities_id` (`entities_id`), + KEY `is_recursive` (`is_recursive`) + ) ENGINE = InnoDB ROW_FORMAT = DYNAMIC DEFAULT CHARSET = {$default_charset} COLLATE = {$default_collation};"; + $DB->doQuery($query); +} diff --git a/install/migrations/update_10.0.x_to_11.0.0/savedsearch.php b/install/migrations/update_10.0.x_to_11.0.0/savedsearch.php new file mode 100644 index 00000000000..fc938849ed8 --- /dev/null +++ b/install/migrations/update_10.0.x_to_11.0.0/savedsearch.php @@ -0,0 +1,64 @@ +. + * + * --------------------------------------------------------------------- + */ + +use Glpi\DBAL\QuerySubQuery; +use Glpi\DBAL\QueryExpression; + +/** + * @var \DBmysql $DB + * @var \Migration $migration + * @var array $DELFROMDISPLAYPREF + */ + +$table = SavedSearch::getTable(); +$field = 'is_private'; +if ($DB->fieldExists($table, $field)) { + $DB->insert('glpi_entities_savedsearches', new QuerySubQuery([ + 'SELECT' => [ + new QueryExpression('null AS `id`'), + 'id as savedsearches_id', + 'entities_id', + 'is_recursive', + ], + 'FROM' => 'glpi_savedsearches', + 'WHERE' => [ + 'is_private' => ['<>', 0], + ], + ])); + + $migration->dropField($table, $field); + + $DELFROMDISPLAYPREF['SavedSearch'] = 4; +} diff --git a/install/migrations/update_10.0.x_to_11.0.0/savedsearch_usertarget.php b/install/migrations/update_10.0.x_to_11.0.0/savedsearch_usertarget.php new file mode 100644 index 00000000000..4033a3978e0 --- /dev/null +++ b/install/migrations/update_10.0.x_to_11.0.0/savedsearch_usertarget.php @@ -0,0 +1,55 @@ +. + * + * --------------------------------------------------------------------- + */ + +/** + * @var \DBmysql $DB + * @var \Migration $migration + */ + +$default_charset = DBConnection::getDefaultCharset(); +$default_collation = DBConnection::getDefaultCollation(); +$default_key_sign = DBConnection::getDefaultPrimaryKeySignOption(); + +if (!$DB->tableExists('glpi_savedsearches_usertargets')) { + $query = "CREATE TABLE `glpi_savedsearches_usertargets` ( + `id` int {$default_key_sign} NOT NULL AUTO_INCREMENT, + `savedsearches_id` int {$default_key_sign} NOT NULL DEFAULT '0', + `users_id` int {$default_key_sign} NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `savedsearches_id` (`savedsearches_id`), + KEY `users_id` (`users_id`) + ) ENGINE = InnoDB ROW_FORMAT = DYNAMIC DEFAULT CHARSET = {$default_charset} COLLATE = {$default_collation};"; + $DB->doQuery($query); +} diff --git a/install/mysql/glpi-empty.sql b/install/mysql/glpi-empty.sql index 5419306050e..16451f977bd 100644 --- a/install/mysql/glpi-empty.sql +++ b/install/mysql/glpi-empty.sql @@ -235,7 +235,6 @@ CREATE TABLE `glpi_savedsearches` ( `type` int NOT NULL DEFAULT '0', `itemtype` varchar(100) NOT NULL, `users_id` int unsigned NOT NULL DEFAULT '0', - `is_private` tinyint NOT NULL DEFAULT '1', `entities_id` int unsigned NOT NULL DEFAULT '0', `is_recursive` tinyint NOT NULL DEFAULT '0', `query` text, @@ -249,7 +248,6 @@ CREATE TABLE `glpi_savedsearches` ( KEY `itemtype` (`itemtype`), KEY `entities_id` (`entities_id`), KEY `users_id` (`users_id`), - KEY `is_private` (`is_private`), KEY `is_recursive` (`is_recursive`), KEY `last_execution_time` (`last_execution_time`), KEY `last_execution_date` (`last_execution_date`), @@ -292,6 +290,18 @@ CREATE TABLE `glpi_savedsearches_alerts` ( KEY `date_creation` (`date_creation`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; +### Dump table glpi_savedsearches_usertargets + +DROP TABLE IF EXISTS `glpi_savedsearches_usertargets`; +CREATE TABLE `glpi_savedsearches_usertargets` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `users_id` int unsigned NOT NULL DEFAULT '0', + `savedsearches_id` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `savedsearches_id` (`savedsearches_id`), + KEY `users_id` (`users_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + ### Dump table glpi_budgets @@ -2910,6 +2920,20 @@ CREATE TABLE `glpi_entities_rssfeeds` ( KEY `is_recursive` (`is_recursive`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; +### Dump table glpi_entities_savedsearches + +DROP TABLE IF EXISTS `glpi_entities_savedsearches`; +CREATE TABLE `glpi_entities_savedsearches` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `savedsearches_id` int unsigned NOT NULL DEFAULT '0', + `entities_id` int unsigned NOT NULL DEFAULT '0', + `is_recursive` tinyint NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `savedsearches_id` (`savedsearches_id`), + KEY `entities_id` (`entities_id`), + KEY `is_recursive` (`is_recursive`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + ### Dump table glpi_events @@ -3134,6 +3158,22 @@ CREATE TABLE `glpi_groups_rssfeeds` ( KEY `is_recursive` (`is_recursive`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; +### Dump table glpi_groups_savedsearches + +DROP TABLE IF EXISTS `glpi_groups_savedsearches`; +CREATE TABLE `glpi_groups_savedsearches` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `savedsearches_id` int unsigned NOT NULL DEFAULT '0', + `groups_id` int unsigned NOT NULL DEFAULT '0', + `entities_id` int unsigned DEFAULT NULL, + `is_recursive` tinyint NOT NULL DEFAULT '0', + `no_entity_restriction` tinyint NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `savedsearches_id` (`savedsearches_id`), + KEY `groups_id` (`groups_id`), + KEY `entities_id` (`entities_id`), + KEY `is_recursive` (`is_recursive`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; ### Dump table glpi_groups_tickets diff --git a/phpunit/functional/NotificationTargetSavedSearch_AlertTest.php b/phpunit/functional/NotificationTargetSavedSearch_AlertTest.php index 98165fae662..794a982348b 100644 --- a/phpunit/functional/NotificationTargetSavedSearch_AlertTest.php +++ b/phpunit/functional/NotificationTargetSavedSearch_AlertTest.php @@ -51,6 +51,7 @@ public function testAddDataForTemplate() 'entities_id' => getItemByTypeName('Entity', '_test_root_entity', true), 'users_id' => \Session::getLoginUserID(), 'itemtype' => 'Computer', + 'is_private' => 1, 'url' => 'http://localhost/front/computer.php?is_deleted=0&as_map=0&browse=0&criteria%5B0%5D%5Blink%5D=AND&criteria%5B0%5D%5Bfield%5D=view&criteria%5B0%5D%5Bsearchtype%5D=contains&criteria%5B0%5D%5Bvalue%5D=test&itemtype=Computer&start=0&_glpi_csrf_token=735e344f1f47545e5bea56aa4e75c15ca45d3628307937c3bf185e0a3bca39db&sort%5B%5D=1&order%5B%5D=ASC', ]); $this->assertGreaterThan(0, $saved_searches_id); diff --git a/phpunit/functional/SavedSearchTest.php b/phpunit/functional/SavedSearchTest.php index ae23dbc307c..48243a2c033 100644 --- a/phpunit/functional/SavedSearchTest.php +++ b/phpunit/functional/SavedSearchTest.php @@ -35,8 +35,6 @@ namespace tests\units; use DbTestCase; -use MassiveAction; -use SavedSearch; /* Test for inc/savedsearch.class.php */ @@ -58,43 +56,47 @@ public function testGetVisibilityCriteria() public function testAddVisibilityRestrict() { + global $DB; + $test_root = getItemByTypeName('Entity', '_test_root_entity', true); $test_child_1 = getItemByTypeName('Entity', '_test_child_1', true); $test_child_2 = getItemByTypeName('Entity', '_test_child_2', true); - - //first, as a super-admin + // super-admin $this->login(); $this->assertSame('', \SavedSearch::addVisibilityRestrict()); + // no rights on bookmark $this->login('normal', 'normal'); + $visibility_restrict = "`glpi_savedsearches`.`users_id` = '5'"; $this->assertSame( - "`glpi_savedsearches`.`is_private` = '1' AND `glpi_savedsearches`.`users_id` = '5' AND (true)", + $visibility_restrict, \SavedSearch::addVisibilityRestrict() ); - //add public saved searches read right for normal profile - global $DB; - $DB->update( - 'glpi_profilerights', - ['rights' => 1], + // temporarily add admin profile and switch to it to test can see public + $DB->insert( + 'glpi_profiles_users', [ - 'profiles_id' => 2, - 'name' => 'bookmark_public', + 'users_id' => \Session::getLoginUserID(), + 'profiles_id' => 3, + 'entities_id' => 0, + 'is_recursive' => 1, ] ); - - //ACLs have changed: login again. + // logout -> login to be able to switch to new profile + $this->logOut(); $this->login('normal', 'normal'); - + \Session::changeProfile(3); + $visibility_restrict2 = "((`glpi_savedsearches`.`users_id` = '5' AND (true)) OR ((`glpi_savedsearches_usertargets`.`users_id` = '5' OR (`glpi_groups_savedsearches`.`groups_id` IN ('-1') AND ((`glpi_groups_savedsearches`.`no_entity_restriction` = '1') OR ((`glpi_groups_savedsearches`.`entities_id` IN ('0', '4', '1', '2', '3', '5', '6'))))) OR ((`glpi_entities_savedsearches`.`entities_id` IN ('0', '4', '1', '2', '3', '5', '6'))))))"; $this->assertSame( - "((`glpi_savedsearches`.`is_private` = '1' AND `glpi_savedsearches`.`users_id` = '5') OR (`glpi_savedsearches`.`is_private` = '0')) AND (true)", + $visibility_restrict2, \SavedSearch::addVisibilityRestrict() ); - - // Check entity restriction + // can see public after moving entity $this->setEntity('_test_root_entity', true); + $visibility_restrict3 = "((`glpi_savedsearches`.`users_id` = '5' AND ((`glpi_savedsearches`.`entities_id` IN ('4', '5', '6') OR (`glpi_savedsearches`.`is_recursive` = '1' AND `glpi_savedsearches`.`entities_id` IN ('0'))))) OR ((`glpi_savedsearches_usertargets`.`users_id` = '5' OR (`glpi_groups_savedsearches`.`groups_id` IN ('-1') AND ((`glpi_groups_savedsearches`.`no_entity_restriction` = '1') OR (((`glpi_groups_savedsearches`.`entities_id` IN ('$test_root', '$test_child_1', '$test_child_2') OR (`glpi_groups_savedsearches`.`is_recursive` = '1' AND `glpi_groups_savedsearches`.`entities_id` IN ('0'))))))) OR (((`glpi_entities_savedsearches`.`entities_id` IN ('4', '5', '6') OR (`glpi_entities_savedsearches`.`is_recursive` = '1' AND `glpi_entities_savedsearches`.`entities_id` IN ('0'))))))))"; $this->assertSame( - "((`glpi_savedsearches`.`is_private` = '1' AND `glpi_savedsearches`.`users_id` = '5') OR (`glpi_savedsearches`.`is_private` = '0')) AND ((`glpi_savedsearches`.`entities_id` IN ('$test_root', '$test_child_1', '$test_child_2') OR (`glpi_savedsearches`.`is_recursive` = '1' AND `glpi_savedsearches`.`entities_id` IN ('0'))))", + $visibility_restrict3, \SavedSearch::addVisibilityRestrict() ); } @@ -104,7 +106,8 @@ public function testGetMine() global $DB; $root_entity_id = getItemByTypeName(\Entity::class, '_test_root_entity', true); - $child_entity_id = getItemByTypeName(\Entity::class, '_test_child_1', true); + + $test_group_1_id = getItemByTypeName(\Group::class, '_test_group_1', true); // needs a user // let's use TU_USER @@ -115,73 +118,87 @@ public function testGetMine() // now add a bookmark on Ticket view $bk = new \SavedSearch(); $this->assertTrue( - (bool) $bk->add([ - 'name' => 'public root recursive', - 'type' => 1, - 'itemtype' => 'Ticket', - 'users_id' => $tuuser_id, - 'is_private' => 0, - 'entities_id' => $root_entity_id, - 'is_recursive' => 1, - 'url' => 'front/ticket.php?itemtype=Ticket&sort=2&order=DESC&start=0&criteria[0][field]=5&criteria[0][searchtype]=equals&criteria[0][value]=' . $tuuser_id, - ]) + (bool) $bk->add( + [ + 'name' => 'private root recursive', + 'type' => 1, + 'itemtype' => 'Ticket', + 'users_id' => $tuuser_id, + 'entities_id' => 0, + 'is_recursive' => 1, + 'url' => 'front/ticket.php?itemtype=Ticket&sort=2&order=DESC&start=0&criteria[0][field]=5&criteria[0][searchtype]=equals&criteria[0][value]=' . $tuuser_id, + ] + ) ); + $bk_private_id = $bk->getID(); $this->assertTrue( - (bool) $bk->add([ - 'name' => 'public root NOT recursive', - 'type' => 1, - 'itemtype' => 'Ticket', - 'users_id' => $tuuser_id, - 'is_private' => 0, - 'entities_id' => $root_entity_id, - 'is_recursive' => 0, - 'url' => 'front/ticket.php?itemtype=Ticket&sort=2&order=DESC&start=0&criteria[0][field]=5&criteria[0][searchtype]=equals&criteria[0][value]=' . $tuuser_id, - ]) + (bool) $bk->add( + [ + 'name' => 'target user root recursive', + 'type' => 1, + 'itemtype' => 'Ticket', + 'users_id' => $tuuser_id, + 'entities_id' => 0, + 'is_recursive' => 1, + 'url' => 'front/ticket.php?itemtype=Ticket&sort=2&order=DESC&start=0&criteria[0][field]=5&criteria[0][searchtype]=equals&criteria[0][value]=' . $tuuser_id, + ] + ) ); + $bk_target_user_id = $bk->getID(); $this->assertTrue( - (bool) $bk->add([ - 'name' => 'public child 1 recursive', - 'type' => 1, - 'itemtype' => 'Ticket', - 'users_id' => $tuuser_id, - 'is_private' => 0, - 'entities_id' => $child_entity_id, - 'is_recursive' => 1, - 'url' => 'front/ticket.php?itemtype=Ticket&sort=2&order=DESC&start=0&criteria[0][field]=5&criteria[0][searchtype]=equals&criteria[0][value]=' . $tuuser_id, - ]) + (bool) $bk->add( + [ + 'name' => 'target group root recursive', + 'type' => 1, + 'itemtype' => 'Ticket', + 'users_id' => $tuuser_id, + 'entities_id' => 0, + 'is_recursive' => 1, + 'url' => 'front/ticket.php?itemtype=Ticket&sort=2&order=DESC&start=0&criteria[0][field]=5&criteria[0][searchtype]=equals&criteria[0][value]=' . $tuuser_id, + ] + ) ); - + $bk_target_group_id = $bk->getID(); + // has is_private => 0 in inputs, so a target will be automatically created for the bookmark's entity $this->assertTrue( - (bool) $bk->add([ - 'name' => 'private TU_USER', - 'type' => 1, - 'itemtype' => 'Ticket', - 'users_id' => $tuuser_id, - 'is_private' => 1, - 'entities_id' => 0, - 'is_recursive' => 1, - 'url' => 'front/ticket.php?itemtype=Ticket&sort=2&order=DESC&start=0&criteria[0][field]=5&criteria[0][searchtype]=equals&criteria[0][value]=' . $tuuser_id, - ]) + (bool) $bk->add( + [ + 'name' => 'created public target entity root recursive', + 'type' => 1, + 'itemtype' => 'Ticket', + 'users_id' => $tuuser_id, + 'is_private' => 0, + 'entities_id' => 0, + 'is_recursive' => 1, + 'url' => 'front/ticket.php?itemtype=Ticket&sort=2&order=DESC&start=0&criteria[0][field]=5&criteria[0][searchtype]=equals&criteria[0][value]=' . $tuuser_id, + ] + ) ); + $bk_target_entity_id = $bk->getID(); + $bk2 = new \SavedSearch(); + $bk2->getFromDB($bk_target_entity_id); + $this->assertEquals(1, $bk2->countVisibilities()); $this->assertTrue( - (bool) $bk->add([ - 'name' => 'private normal user', - 'type' => 1, - 'itemtype' => 'Ticket', - 'users_id' => $normal_id, - 'is_private' => 1, - 'entities_id' => 0, - 'is_recursive' => 1, - 'url' => 'front/ticket.php?itemtype=Ticket&sort=2&order=DESC&start=0&criteria[0][field]=5&criteria[0][searchtype]=equals&criteria[0][value]=' . $tuuser_id, - ]) + (bool) $bk->add( + [ + 'name' => 'private normal user', + 'type' => 1, + 'itemtype' => 'Ticket', + 'users_id' => $normal_id, + 'entities_id' => 0, + 'is_recursive' => 1, + 'url' => 'front/ticket.php?itemtype=Ticket&sort=2&order=DESC&start=0&criteria[0][field]=5&criteria[0][searchtype]=equals&criteria[0][value]=' . $tuuser_id, + ] + ) ); - // With UPDATE 'config' right, we still shouldn't see other user's private searches + $bk_private_normal_id = $bk->getID(); + // With UPDATE 'config' right, we still shouldn't see other user's searches without targets $expected = [ - 'public root recursive', - 'public root NOT recursive', - 'public child 1 recursive', - 'private TU_USER', + 'private root recursive', + 'target user root recursive', + 'target group root recursive', + 'created public target entity root recursive', ]; $mine = $bk->getMine(); $this->assertCount(count($expected), $mine); @@ -196,94 +213,83 @@ public function testGetMine() array_column($mine, 'name') ); - // Normal user cannot see public saved searches by default + // test each type of targets so that normal will be able to see them + $bks_normal = [ + 'private normal user', + 'created public target entity root recursive', + ]; + // add normal to a group + $group_user = new \Group_User(); + $group_user->add( + [ + 'users_id' => 5, + 'groups_id' => $test_group_1_id, + ] + ); $this->login('normal', 'normal'); - $mine = $bk->getMine(); - $this->assertCount(1, $mine); + $this->assertCount(count($bks_normal), $mine); $this->assertEqualsCanonicalizing( - ['private normal user'], + $bks_normal, array_column($mine, 'name') ); - //add public saved searches read right for normal profile - $DB->update( - 'glpi_profilerights', - ['rights' => 1], + // add normal as target for another savedsearch + $DB->insert( + 'glpi_savedsearches_usertargets', [ - 'profiles_id' => 2, - 'name' => 'bookmark_public', + 'users_id' => 5, + 'savedsearches_id' => $bk_target_user_id, ] ); - $this->login('normal', 'normal'); // ACLs have changed: login again. - $expected = [ - 'public root recursive', - 'public root NOT recursive', - 'public child 1 recursive', - 'private normal user', - ]; + $bks_normal[] = 'target user root recursive'; $mine = $bk->getMine('Ticket'); - $this->assertCount(count($expected), $mine); + $this->assertCount(count($bks_normal), $mine); $this->assertEqualsCanonicalizing( - $expected, + $bks_normal, array_column($mine, 'name') ); - // Check entity restrictions - $this->setEntity('_test_root_entity', false); - $expected = [ - 'public root recursive', - 'public root NOT recursive', - 'private normal user', - ]; - $mine = $bk->getMine('Ticket'); - $this->assertCount(count($expected), $mine); - $this->assertEqualsCanonicalizing( - $expected, - array_column($mine, 'name') + // add the group as target for a bookmark + $DB->insert( + 'glpi_groups_savedsearches', + [ + 'savedsearches_id' => $bk_target_group_id, + 'groups_id' => $test_group_1_id, + 'entities_id' => 0, + 'is_recursive' => 1, + ] ); - - $this->setEntity('_test_child_1', true); - $expected = [ - 'public root recursive', - 'public child 1 recursive', - 'private normal user', - ]; + $bks_normal[] = 'target group root recursive'; $mine = $bk->getMine('Ticket'); - $this->assertCount(count($expected), $mine); + $this->assertCount(count($bks_normal), $mine); $this->assertEqualsCanonicalizing( - $expected, + $bks_normal, array_column($mine, 'name') ); - $this->setEntity('_test_child_1', false); - $expected = [ - 'public root recursive', - 'public child 1 recursive', - 'private normal user', - ]; + // add an entity target for an entity at a level below the current one + $DB->insert( + 'glpi_entities_savedsearches', + [ + 'savedsearches_id' => $bk_private_id, + 'entities_id' => $root_entity_id, + 'is_recursive' => 1, + ] + ); + $bks_normal[] = 'private root recursive'; $mine = $bk->getMine('Ticket'); - $this->assertCount(count($expected), $mine); + $this->assertCount(count($bks_normal), $mine); $this->assertEqualsCanonicalizing( - $expected, + $bks_normal, array_column($mine, 'name') ); - } - - public function testAvailableMassiveActions(): void - { - // Act: get saved searches massive actions - $this->login(); - $actions = MassiveAction::getAllMassiveActions(SavedSearch::class); - // Assert: validate the available actions - $this->assertEquals([ - 'Delete permanently', - 'Add to transfer list', - 'Unset as default', - 'Change count method', - 'Change visibility', - 'Change entity', - ], array_values($actions)); + $DB->delete( + $group_user->getTable(), + [ + 'id' => $group_user->getID(), + ] + ); } } diff --git a/src/CommonDBVisible.php b/src/CommonDBVisible.php index 393c0531ecd..150fc6ee8f5 100644 --- a/src/CommonDBVisible.php +++ b/src/CommonDBVisible.php @@ -34,12 +34,19 @@ */ use Glpi\Application\View\TemplateRenderer; +use Glpi\Event; /** * Common DataBase visibility for items */ abstract class CommonDBVisible extends CommonDBTM { + /** + * Types of target available for the itemtype + * @var string[] + */ + public static $types = ['Entity', 'Group', 'Profile', 'User']; + /** * Entities on which item is visible. * Keys are ID, values are DB fields values. @@ -68,6 +75,58 @@ abstract class CommonDBVisible extends CommonDBTM */ protected $users = []; + /** + * Class defining relation to $users + * @var string + */ + protected $userClass; + + /** + * Class defining relation to $profiles + * @var string + */ + protected $profileClass; + + /** + * Class defining relation to $groups + * @var string + */ + protected $groupClass; + + /** + * Class defining relation to entities + * @var string + */ + protected $entityClass; + + /** + * Service for visibility target log + * @var string + */ + protected $service; + + public function __construct() + { + // define default values + if (!$this->userClass) { + $this->userClass = $this->getType() . '_UserTarget'; + } + if (!$this->groupClass) { + $this->groupClass = 'Group_' . $this->getType(); + } + if (!$this->entityClass) { + $this->entityClass = 'Entity_' . $this->getType(); + } + if (!$this->profileClass) { + $this->profileClass = 'Profile_' . $this->getType(); + } + if (!$this->service) { + $this->service = 'tools'; + } + + parent::__construct(); + } + /** * Is the login user have access to item based on visibility configuration * @@ -162,6 +221,16 @@ public function countVisibilities() + count($this->profiles)); } + + /** + * Get right which will be used to determine which users can be targeted + * @return string + */ + public function getVisibilityRight() + { + return strtolower($this::getType()) . '_public'; + } + /** * Show visibility configuration * @@ -178,9 +247,10 @@ public function showVisibility() if ($canedit) { TemplateRenderer::getInstance()->display('components/add_visibility_target.html.twig', [ - 'type' => static::class, - 'rand' => $rand, - 'id' => $ID, + 'type' => static::class, + 'types' => static::$types, + 'rand' => $rand, + 'id' => $ID, 'add_target_msg' => __('Add a target'), 'visiblity_dropdown_params' => $this->getShowVisibilityDropdownParams(), ]); @@ -191,7 +261,7 @@ public function showVisibility() foreach ($this->users as $val) { foreach ($val as $data) { $entries[] = [ - 'itemtype' => static::class . '_User', + 'itemtype' => $this instanceof SavedSearch ? SavedSearch_UserTarget::class : static::class . '_User', 'id' => $data['id'], 'type' => User::getTypeName(1), 'recipient' => htmlescape(getUserName($data['users_id'])), @@ -337,10 +407,10 @@ public function showVisibility() */ protected function getShowVisibilityDropdownParams() { - $params = [ - 'type' => '__VALUE__', - 'right' => strtolower($this::getType()) . '_public', - ]; + $params = ['type' => '__VALUE__']; + if ($right = $this->getVisibilityRight()) { + $params['right'] = $right; + } if (isset($this->fields['entities_id'])) { $params['entity'] = $this->fields['entities_id']; } @@ -349,4 +419,57 @@ protected function getShowVisibilityDropdownParams() } return $params; } + + /** + * Add a visibility target to the item + * @param array $inputs key '_type' determine the type of target + * @return void + */ + public function addVisibility(array $inputs) + { + $fkField = getForeignKeyFieldForItemType($this->getType()); + $item = null; + switch ($inputs['_type']) { + case User::class: + $class = $this->getType() . '_UserTarget'; + if (is_a($class, CommonDBRelation::class, true)) { + $item = new $class(); + } + break; + case Group::class: + $class = 'Group_' . $this->getType(); + if (is_a($class, CommonDBRelation::class, true)) { + $item = new $class(); + } + break; + case Entity::class: + $class = 'Entity_' . $this->getType(); + if (is_a($class, CommonDBRelation::class, true)) { + $item = new $class(); + } + break; + case Profile::class: + $class = 'Profile_' . $this->getType(); + if (is_a($class, CommonDBRelation::class, true)) { + $item = new $class(); + } + break; + } + if (array_key_exists('entities_id', $inputs) && $inputs['entities_id'] == -1) { + // "No restriction" value selected + $inputs['entities_id'] = 'NULL'; + $inputs['no_entity_restriction'] = 1; + } + if (!is_null($item)) { + $item->add($inputs); + Event::log( + $inputs[$fkField], + $this->getType(), + 4, + $this->service, + //TRANS: %s is the user login + sprintf(__('%s adds a target'), $_SESSION["glpiname"]) + ); + } + } } diff --git a/src/Entity_SavedSearch.php b/src/Entity_SavedSearch.php new file mode 100644 index 00000000000..da9367f93b3 --- /dev/null +++ b/src/Entity_SavedSearch.php @@ -0,0 +1,73 @@ +. + * + * --------------------------------------------------------------------- + */ + +class Entity_SavedSearch extends CommonDBRelation +{ + // From CommonDBRelation + public static $itemtype_1 = SavedSearch::class; + public static $items_id_1 = 'savedsearches_id'; + public static $itemtype_2 = Entity::class; + public static $items_id_2 = 'entities_id'; + + public static $checkItem_2_Rights = self::DONT_CHECK_ITEM_RIGHTS; + public static $logs_for_item_2 = false; + + + /** + * Get entities for a saved search + * + * @param SavedSearch $savedSearch SavedSearch instance + * + * @return array of entities linked to a saved search + **/ + public static function getEntities(SavedSearch $savedSearch) + { + /** @var \DBmysql $DB */ + global $DB; + + $results = []; + $iterator = $DB->request([ + 'FROM' => self::getTable(), + 'WHERE' => [ + self::$items_id_1 => $savedSearch->getID(), + ], + ]); + + foreach ($iterator as $data) { + $results[$data[self::$items_id_2]][] = $data; + } + return $results; + } +} diff --git a/src/Glpi/Controller/ItemType/Form/SavedSearchFormController.php b/src/Glpi/Controller/ItemType/Form/SavedSearchFormController.php index 32e7416ee1c..c7d732f8322 100644 --- a/src/Glpi/Controller/ItemType/Form/SavedSearchFormController.php +++ b/src/Glpi/Controller/ItemType/Form/SavedSearchFormController.php @@ -34,7 +34,7 @@ namespace Glpi\Controller\ItemType\Form; -use Glpi\Controller\GenericFormController; +use Glpi\Controller\VisibilityController; use Glpi\Http\RedirectResponse; use Glpi\Routing\Attribute\ItemtypeFormRoute; use Html; @@ -42,7 +42,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -class SavedSearchFormController extends GenericFormController +class SavedSearchFormController extends VisibilityController { #[ItemtypeFormRoute(SavedSearch::class)] public function __invoke(Request $request): Response diff --git a/src/Glpi/Controller/VisibilityController.php b/src/Glpi/Controller/VisibilityController.php new file mode 100644 index 00000000000..09934215493 --- /dev/null +++ b/src/Glpi/Controller/VisibilityController.php @@ -0,0 +1,72 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\Controller; + +use Glpi\Exception\Http\AccessDeniedHttpException; +use Glpi\Exception\Http\BadRequestHttpException; +use Html; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +class VisibilityController extends GenericFormController +{ + public function __invoke(Request $request): Response + { + if ($request->request->has('addvisibility')) { + return $this->addVisibility($request); + } + + return parent::__invoke($request); + } + + public function addVisibility(Request $request): RedirectResponse + { + $class = $request->attributes->get('class'); + $item = getItemForItemtype($class); + $fk_field = getForeignKeyFieldForItemType($class); + if ($item instanceof \CommonDBVisible) { + if ($item->canEdit($request->request->get($fk_field))) { + $item->addVisibility($request->request->all()); + return new RedirectResponse(Html::getBackUrl()); + } else { + throw new AccessDeniedHttpException(); + } + } else { + throw new BadRequestHttpException("Invalid class"); + } + } +} diff --git a/src/Glpi/Search/Provider/SQLProvider.php b/src/Glpi/Search/Provider/SQLProvider.php index ddd2c7abbd4..dd0051870a3 100644 --- a/src/Glpi/Search/Provider/SQLProvider.php +++ b/src/Glpi/Search/Provider/SQLProvider.php @@ -2604,6 +2604,16 @@ public static function getDefaultJoinCriteria(string $itemtype, string $ref_tabl } break; + case SavedSearch::class: + $criterias = \SavedSearch::getVisibilityCriteria(false); + if (isset($criterias['LEFT JOIN'])) { + $out = ['LEFT JOIN' => $criterias['LEFT JOIN']]; + foreach ($criterias['LEFT JOIN'] as $table => $criteria) { + $already_link_tables[] = $table; + } + } + break; + case ITILFollowup::class: foreach ($CFG_GLPI['itil_types'] as $itil_itemtype) { $out = array_merge_recursive($out, self::getLeftJoinCriteria( diff --git a/src/Group_SavedSearch.php b/src/Group_SavedSearch.php new file mode 100644 index 00000000000..9dd76e67c1f --- /dev/null +++ b/src/Group_SavedSearch.php @@ -0,0 +1,73 @@ +. + * + * --------------------------------------------------------------------- + */ + +class Group_SavedSearch extends CommonDBRelation +{ + // From CommonDBRelation + public static $itemtype_1 = SavedSearch::class; + public static $items_id_1 = 'savedsearches_id'; + public static $itemtype_2 = Group::class; + public static $items_id_2 = 'groups_id'; + + public static $checkItem_2_Rights = self::DONT_CHECK_ITEM_RIGHTS; + public static $logs_for_item_2 = false; + + + /** + * Get groups for a saved search + * + * @param SavedSearch $savedSearch SavedSearch instance + * + * @return array of groups linked to a saved search + **/ + public static function getGroups(SavedSearch $savedSearch) + { + /** @var \DBmysql $DB */ + global $DB; + + $results = []; + $iterator = $DB->request([ + 'FROM' => self::getTable(), + 'WHERE' => [ + self::$items_id_1 => $savedSearch->getID(), + ], + ]); + + foreach ($iterator as $data) { + $results[$data[self::$items_id_2]][] = $data; + } + return $results; + } +} diff --git a/src/SavedSearch.php b/src/SavedSearch.php index 854f6192d65..e57b357a717 100644 --- a/src/SavedSearch.php +++ b/src/SavedSearch.php @@ -45,12 +45,12 @@ * * @since 9.2 **/ -class SavedSearch extends CommonDBTM implements ExtraVisibilityCriteria +class SavedSearch extends CommonDBVisible implements ExtraVisibilityCriteria { use Clonable; public static $rightname = 'bookmark_public'; - + public static $types = [Group::class, User::class, Entity::class]; public const SEARCH = 1; //SEARCH SYSTEM bookmark public const URI = 2; public const ALERT = 3; //SEARCH SYSTEM search alert @@ -59,6 +59,11 @@ class SavedSearch extends CommonDBTM implements ExtraVisibilityCriteria public const COUNT_YES = 1; public const COUNT_AUTO = 2; + protected $userClass = SavedSearch_UserTarget::class; + protected $groupClass = Group_SavedSearch::class; + protected $entityClass = Entity_SavedSearch::class; + protected $service = 'tools'; + public static function getForbiddenActionsForMenu() { return ['add']; @@ -254,33 +259,111 @@ public static function processMassiveActionsForOneItemtype( parent::processMassiveActionsForOneItemtype($ma, $item, $ids); } - public function canCreateItem(): bool + public function haveVisibilityAccess(): bool { + if (!self::canView()) { + return false; + } + + return parent::haveVisibilityAccess(); + } - if ($this->fields['is_private'] == 1) { - return (Session::haveRight('config', UPDATE) - || $this->fields['users_id'] == Session::getLoginUserID()); + public function canCreateItem(): bool + { + if (isset($this->input['is_private']) && $this->input['is_private'] == 0) { + return self::canCreatePublic(); } return parent::canCreateItem(); } + public static function canCreatePublic(): bool + { + return (Session::haveRight('config', UPDATE) || + Session::haveRight(self::$rightname, CREATE)); + } + public function canViewItem(): bool { - if ($this->fields['is_private'] == 1) { - return (Session::haveRight('config', READ) - || $this->fields['users_id'] == Session::getLoginUserID()); + if ( + Session::haveRight('config', READ) + || $this->fields['users_id'] == Session::getLoginUserID() + ) { + return true; + } + + if (array_key_exists($this->getID(), $this->getMine())) { + return true; + } + + return false; + } + + public function post_getFromDB() + { + // Group + $this->groups = Group_SavedSearch::getGroups($this); + + // Users + $this->users = SavedSearch_UserTarget::getUsers($this); + + // Entities + $this->entities = Entity_SavedSearch::getEntities($this); + } + + public function post_addItem() + { + // for search saved as public, automatically create a link with its entity + if (isset($this->input['is_private']) && !$this->input['is_private']) { + $item = new Entity_SavedSearch(); + $item->add([ + 'savedsearches_id' => $this->getID(), + 'entities_id' => $this->fields['entities_id'], + 'is_recursive' => $this->fields['is_recursive'], + ]); + } + parent::post_addItem(); + } + + public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) + { + if (self::canView() + && $item::class == SavedSearch::class + && self::canCreatePublic()) { + $nb = 0; + if ($_SESSION['glpishow_count_on_tabs']) { + $nb = $item->countVisibilities(); + } + return [ + 1 => self::createTabEntry( + _n( + 'Target', + 'Targets', + Session::getPluralNumber() + ), + $nb + ), + ]; } - return parent::canViewItem(); + return ''; } public function defineTabs($options = []) { $ong = []; - $this->addDefaultFormTab($ong) - ->addStandardTab(SavedSearch_Alert::class, $ong, $options); + $this->addDefaultFormTab($ong); + $this->addStandardTab(SavedSearch::class, $ong, $options); + $this->addStandardTab(SavedSearch_Alert::class, $ong, $options); return $ong; } + public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) + { + if ($item::class == SavedSearch::class) { + $item->showVisibility(); + } + return false; + } + public function rawSearchOptions() { $tab = parent::rawSearchOptions(); @@ -300,15 +383,6 @@ public function rawSearchOptions() 'datatype' => 'dropdown', ]; - $tab[] = [ - 'id' => 4, - 'table' => $this->getTable(), - 'field' => 'is_private', - 'name' => __('Is private'), - 'datatype' => 'bool', - 'massiveaction' => false, - ]; - $tab[] = ['id' => '8', 'table' => $this->getTable(), 'field' => 'itemtype', @@ -451,6 +525,8 @@ public function cleanDBonPurge() [ SavedSearch_Alert::class, SavedSearch_User::class, + SavedSearch_UserTarget::class, + Group_SavedSearch::class, ] ); } @@ -472,6 +548,7 @@ public function showForm($ID, array $options = []) TemplateRenderer::getInstance()->display('pages/tools/savedsearch/form.html.twig', [ 'item' => $this, 'can_create' => self::canCreate(), + 'can_create_public' => self::canCreatePublic(), 'params' => $options, ]); return true; @@ -665,6 +742,41 @@ public function unmarkDefaults(array $ids) return false; } + /** + * Get relations for targets + * @return array[][] key 'LEFT JOIN' for request + */ + public static function getDefaultJoin() + { + $table = self::getTable(); + $user_table = SavedSearch_UserTarget::getTable(); + $group_table = Group_SavedSearch::getTable(); + $entity_table = Entity_SavedSearch::getTable(); + return [ + 'LEFT JOIN' => [ + // relations for targets + $user_table => [ + 'ON' => [ + $user_table => 'savedsearches_id', + $table => 'id', + ], + ], + $group_table => [ + 'ON' => [ + $group_table => 'savedsearches_id', + $table => 'id', + ], + ], + $entity_table => [ + 'ON' => [ + $entity_table => 'savedsearches_id', + $table => 'id', + ], + ], + ], + ]; + } + /** * return an array of saved searches for a given itemtype * @@ -682,6 +794,10 @@ public function getMine(?string $itemtype = null, bool $inverse = false): array $table = $this->getTable(); $utable = 'glpi_savedsearches_users'; + $user_table = SavedSearch_UserTarget::getTable(); + $group_table = Group_SavedSearch::getTable(); + $entity_table = Entity_SavedSearch::getTable(); + $criteria = [ 'SELECT' => [ "$table.*", @@ -690,19 +806,59 @@ public function getMine(?string $itemtype = null, bool $inverse = false): array ), ], 'FROM' => $table, - 'LEFT JOIN' => [ - $utable => [ - 'ON' => [ - $utable => 'savedsearches_id', - $table => 'id', - ], - ], - ], 'ORDERBY' => [ 'itemtype', 'name', ], - ] + self::getVisibilityCriteriaForMine(); + ] + self::getDefaultJoin(); + + $criteria['LEFT JOIN'][$utable] = [ + 'ON' => [ + $utable => 'savedsearches_id', + $table => 'id', + ], + ]; + $owner_restrict = [ + $table . '.users_id' => Session::getLoginUserID(), + ]; + $entity_restrict = getEntitiesRestrictCriteria($table, '', '', true); + if (count($entity_restrict)) { + $owner_restrict += $entity_restrict; + } + $criteria['WHERE'] = [ + 'OR' => [ + $owner_restrict, + [ + 'OR' => [ + // directly targeted + $user_table . '.users_id' => Session::getLoginUserID(), + // targeted through groups + [ + $group_table . '.groups_id' => count($_SESSION["glpigroups"]) + ? $_SESSION["glpigroups"] + : [-1], + 'OR' => [ + [$group_table . '.no_entity_restriction' => 1], + getEntitiesRestrictCriteria( + $group_table, + '', + $_SESSION['glpiactiveentities'], + true + ), + ], + ], + // targeted through entities + getEntitiesRestrictCriteria( + $entity_table, + '', + '', + true, + true + ), + ], + ], + ], + ]; if ($itemtype != null) { if (!$inverse) { @@ -795,6 +951,7 @@ public function displayMine(?string $itemtype = null, bool $inverse = false) { TemplateRenderer::getInstance()->display('layout/parts/saved_searches_list.html.twig', [ 'active' => $_SESSION['glpi_loaded_savedsearch'] ?? "", + 'current_user' => Session::getLoginUserID(), 'saved_searches' => $this->getMine($itemtype, $inverse), ]); } @@ -1196,26 +1353,6 @@ public static function addVisibilityRestrict() return $sql; } - private static function getVisibilityCriteriaForMine(): array - { - $criteria = ['WHERE' => []]; - $restrict = [ - self::getTable() . '.is_private' => 1, - self::getTable() . '.users_id' => Session::getLoginUserID(), - ]; - - if (Session::haveRight(self::$rightname, READ)) { - $restrict = [ - 'OR' => [ - $restrict, - [self::getTable() . '.is_private' => 0], - ], - ]; - } - - $criteria['WHERE'] = $restrict + getEntitiesRestrictCriteria(self::getTable(), '', '', true); - return $criteria; - } /** * Return visibility joins to add to DBIterator parameters @@ -1232,7 +1369,64 @@ public static function getVisibilityCriteria(bool $forceall = false): array return ['WHERE' => []]; } - return self::getVisibilityCriteriaForMine(); + if (!Session::haveRight(self::$rightname, READ)) { + return [ + 'WHERE' => ['glpi_savedsearches.users_id' => Session::getLoginUserID()], + ]; + } + + $table = self::getTable(); + $user_table = SavedSearch_UserTarget::getTable(); + $group_table = Group_SavedSearch::getTable(); + $entity_table = Entity_SavedSearch::getTable(); + + $criteria = self::getDefaultJoin(); + + $owner_restrict = [ + $table . '.users_id' => Session::getLoginUserID(), + ]; + $entity_restrict = getEntitiesRestrictCriteria($table, '', '', true); + if (count($entity_restrict)) { + $owner_restrict += $entity_restrict; + } + + $restrict = [ + 'OR' => [ + $owner_restrict, + [ + 'OR' => [ + // directly targeted + $user_table . '.users_id' => Session::getLoginUserID(), + // targeted through groups + [ + $group_table . '.groups_id' => count($_SESSION["glpigroups"]) + ? $_SESSION["glpigroups"] + : [-1], + 'OR' => [ + [$group_table . '.no_entity_restriction' => 1], + getEntitiesRestrictCriteria( + $group_table, + '', + $_SESSION['glpiactiveentities'], + true + ), + ], + ], + // targeted through entities + getEntitiesRestrictCriteria( + $entity_table, + '', + '', + true, + true + ), + ], + ], + ], + ]; + $criteria['WHERE'] = $restrict; + + return $criteria; } public static function getIcon() @@ -1244,4 +1438,13 @@ public function getCloneRelations(): array { return []; } + + /** + * No specific right needed to be a target + * @return string + */ + public function getVisibilityRight() + { + return ''; + } } diff --git a/src/SavedSearch_User.php b/src/SavedSearch_User.php index 11ea01a5cc7..a5227c54dd4 100644 --- a/src/SavedSearch_User.php +++ b/src/SavedSearch_User.php @@ -37,10 +37,9 @@ class SavedSearch_User extends CommonDBRelation { public $auto_message_on_action = false; - public static $itemtype_1 = 'SavedSearch'; + public static $itemtype_1 = SavedSearch::class; public static $items_id_1 = 'savedsearches_id'; - - public static $itemtype_2 = 'User'; + public static $itemtype_2 = User::class; public static $items_id_2 = 'users_id'; diff --git a/src/SavedSearch_UserTarget.php b/src/SavedSearch_UserTarget.php new file mode 100644 index 00000000000..6055aed49dc --- /dev/null +++ b/src/SavedSearch_UserTarget.php @@ -0,0 +1,74 @@ +. + * + * --------------------------------------------------------------------- + */ + +class SavedSearch_UserTarget extends CommonDBRelation +{ + public $auto_message_on_action = false; + + public static $itemtype_1 = SavedSearch::class; + public static $items_id_1 = 'savedsearches_id'; + + public static $itemtype_2 = User::class; + public static $items_id_2 = 'users_id'; + + public static $checkItem_2_Rights = self::DONT_CHECK_ITEM_RIGHTS; + public static $logs_for_item_2 = false; + + /** + * Get users for a saved search + * + * @param SavedSearch $savedSearch SavedSearch instance + * + * @return array of users linked to a saved search + **/ + public static function getUsers(SavedSearch $savedSearch) + { + /** @var \DBmysql $DB */ + global $DB; + + $results = []; + $iterator = $DB->request([ + 'FROM' => self::getTable(), + 'WHERE' => [ + self::$items_id_1 => $savedSearch->getID(), + ], + ]); + + foreach ($iterator as $data) { + $results[$data[self::$items_id_2]][] = $data; + } + return $results; + } +} diff --git a/src/User.php b/src/User.php index 7e221ae570e..bd6811dfcc4 100644 --- a/src/User.php +++ b/src/User.php @@ -482,14 +482,64 @@ public function cleanDBonPurge() $reminder_translation = new ReminderTranslation(); $reminder_translation->deleteByCriteria(['users_id' => $this->fields['id']]); - // Delete private bookmark $ss = new SavedSearch(); - $ss->deleteByCriteria( - [ - 'users_id' => $this->fields['id'], - 'is_private' => 1, - ] - ); + $search_table = SavedSearch::getTable(); + $user_table = SavedSearch_UserTarget::getTable(); + $group_table = Group_SavedSearch::getTable(); + $entity_table = Entity_SavedSearch::getTable(); + // Retrieve all bookmarks created by the user which have at least 1 target + $publics = $ss->find([ + 'id' => new QuerySubQuery([ + 'SELECT' => $search_table . '.id', + 'FROM' => $ss->getTable(), + 'LEFT JOIN' => [ + $user_table => [ + 'ON' => [ + $user_table => 'savedsearches_id', + $search_table => 'id', + ], + ], + $group_table => [ + 'ON' => [ + $group_table => 'savedsearches_id', + $search_table => 'id', + ], + ], + $entity_table => [ + 'ON' => [ + $entity_table => 'savedsearches_id', + $search_table => 'id', + ], + ], + ], + 'WHERE' => [ + $search_table . '.users_id' => $this->fields['id'], + 'OR' => [ + ['NOT' => [$user_table . '.savedsearches_id' => null]], + ['NOT' => [$group_table . '.savedsearches_id' => null]], + ['NOT' => [$entity_table . '.savedsearches_id' => null]], + ], + ], + ]), + ]); + if (count($publics)) { + $publics = array_map(fn($e) => $e['id'], $publics); + // Delete private bookmark + $ss->deleteByCriteria( + [ + 'users_id' => $this->fields['id'], + 'NOT' => [ + 'id' => $publics, + ], + ] + ); + } else { + $ss->deleteByCriteria( + [ + 'users_id' => $this->fields['id'], + ] + ); + } // Set no user to public bookmark $DB->update( @@ -529,6 +579,7 @@ public function cleanDBonPurge() Reminder_User::class, RSSFeed_User::class, SavedSearch_User::class, + SavedSearch_UserTarget::class, Ticket_User::class, UserEmail::class, ] diff --git a/templates/components/add_visibility_target.html.twig b/templates/components/add_visibility_target.html.twig index 6f331ca5c5c..77053c342f0 100644 --- a/templates/components/add_visibility_target.html.twig +++ b/templates/components/add_visibility_target.html.twig @@ -37,7 +37,7 @@