diff --git a/hook.php b/hook.php index 3105ab5..966de5f 100644 --- a/hook.php +++ b/hook.php @@ -31,43 +31,8 @@ * ------------------------------------------------------------------------- */ -use Glpi\Inventory\Conf; use GlpiPlugin\Moreoptions\Config; -/** - * ------------------------------------------------------------------------- - * MoreOptions plugin for GLPI - * ------------------------------------------------------------------------- - * - * MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * ------------------------------------------------------------------------- - * @copyright Copyright (C) 2025 by the MoreOptions plugin team. - * @copyright Copyright (C) 2022-2024 by More Options plugin team. - * @license MIT https://opensource.org/licenses/mit-license.php - * @license GPLv3 https://www.gnu.org/licenses/gpl-3.0.html - * @link https://github.com/pluginsGLPI/moreoptions - * @link https://gitlab.teclib.com/glpi-network/cancelsend/ - * ------------------------------------------------------------------------- - */ - function plugin_moreoptions_install(): bool { $migration = new Migration(PLUGIN_MOREOPTIONS_VERSION); diff --git a/setup.php b/setup.php index 7763483..b0dea80 100644 --- a/setup.php +++ b/setup.php @@ -96,6 +96,13 @@ function plugin_init_moreoptions(): void Controller::class, 'beforeCloseITILObject', ]; + $PLUGIN_HOOKS[Hooks::PRE_ITEM_ADD]['moreoptions'][ITILSolution::class] = [ + Controller::class, 'beforeCloseITILObject', + ]; + $PLUGIN_HOOKS[Hooks::PRE_ITEM_UPDATE]['moreoptions'][ITILSolution::class] = [ + Controller::class, 'beforeCloseITILObject', + ]; + $PLUGIN_HOOKS[Hooks::PRE_ITEM_UPDATE]['moreoptions'][Config::class] = [ Config::class, 'preItemUpdate', ]; diff --git a/src/Controller.php b/src/Controller.php index 709f880..a52c93d 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -48,6 +48,7 @@ use CommonITILObject; use CommonITILValidation; use Glpi\Form\Category; +use GlpiPlugin\Behaviors\Common; use GlpiPlugin\Moreoptions\Config; use Group_Item; use Group_Problem; @@ -271,27 +272,60 @@ private static function addGroupsForActorType(CommonDBTM $item, Config $moconfig } } - public static function beforeCloseITILObject(CommonITILObject $item): void + public static function beforeCloseITILObject(CommonDBTM $item): void { if (!is_array($item->input)) { return; } + $closed = true; + + if ($item instanceof ITILSolution) { + $itemtype = $item->input['itemtype'] ?? null; + + switch ($itemtype) { + case 'Ticket': + $parent_item = new Ticket(); + break; + case 'Change': + $parent_item = new Change(); + break; + case 'Problem': + $parent_item = new Problem(); + break; + default: + return; + } + + if (!$parent_item->getFromDB($item->input['items_id'])) { + return; + } + $closed = self::requireFieldsToClose($parent_item, true); + $closed = self::preventClosure($parent_item) && $closed; + } + if ( - (isset($item->input['status']) && ($item->input['status'] == CommonITILObject::CLOSED || $item->input['status'] == CommonITILObject::SOLVED)) - || $item->fields['status'] == CommonITILObject::CLOSED - || $item->fields['status'] == CommonITILObject::SOLVED + $item instanceof CommonITILObject + && ( + (isset($item->input['status']) && ($item->input['status'] == CommonITILObject::CLOSED || $item->input['status'] == CommonITILObject::SOLVED)) + || $item->fields['status'] == CommonITILObject::CLOSED + || $item->fields['status'] == CommonITILObject::SOLVED + ) ) { - self::requireFieldsToClose($item); - self::preventClosure($item); + $closed = self::requireFieldsToClose($item); + $closed = self::preventClosure($item) && $closed; + } + + if (!$closed) { + $item->input = false; } } - public static function preventClosure(CommonDBTM $item): void + public static function preventClosure(CommonDBTM $item): bool { $conf = Config::getConfig(); if ($conf->fields['is_active'] != 1) { - return; + return true; } $tasks = []; @@ -319,21 +353,24 @@ public static function preventClosure(CommonDBTM $item): void if (is_array($t) && isset($t['state']) && $t['state'] == Planning::TODO) { Session::addMessageAfterRedirect(__s('The ticket you wish to close has tasks that need to be completed.', 'moreoptions'), false, ERROR); $item->input = false; - return; + return false; } } + return true; } - public static function requireFieldsToClose(CommonDBTM $item): void + public static function requireFieldsToClose(CommonDBTM $item, bool $is_solution = false): bool { $conf = Config::getConfig(); if ($conf->fields['is_active'] != 1) { - return; + return true; } $message = ''; $itemtype = get_class($item); + $data = empty($item->input) ? $item->fields : $item->input; + // Determine the configuration suffix and actor classes based on item type $configSuffix = '_' . strtolower($itemtype); $userClass = $item->userlinkclass ?? ''; @@ -346,10 +383,10 @@ public static function requireFieldsToClose(CommonDBTM $item): void $tech = new $userClass(); } else { // If the user class is not valid, skip this check - return; + return false; } $techs = $tech->find([ - $itemIdField => $item->fields['id'], + $itemIdField => $data['id'], 'type' => CommonITILActor::ASSIGN, ]); if (count($techs) == 0) { @@ -363,10 +400,10 @@ public static function requireFieldsToClose(CommonDBTM $item): void $group = new $groupClass(); } else { // If the group class is not valid, skip this check - return; + return false; } $groups = $group->find([ - $itemIdField => $item->fields['id'], + $itemIdField => $data['id'], 'type' => CommonITILActor::ASSIGN, ]); if (count($groups) == 0) { @@ -376,27 +413,30 @@ public static function requireFieldsToClose(CommonDBTM $item): void // Check for required category if ($conf->fields['require_category_to_close' . $configSuffix] == 1) { - if ((!isset($item->input['itilcategories_id']) || empty($item->input['itilcategories_id']))) { + if ((!isset($data['itilcategories_id']) || empty($data['itilcategories_id']))) { $message .= '- ' . __s('Category') . '
'; } } // Check for required location if ($conf->fields['require_location_to_close' . $configSuffix] == 1) { - if ((!isset($item->input['locations_id']) || empty($item->input['locations_id']))) { + if ((!isset($data['locations_id']) || empty($data['locations_id']))) { $message .= '- ' . __s('Location') . '
'; } } // Check if solution exists before closing - if ($conf->fields['require_solution_to_close' . $configSuffix] == 1 - && is_array($item->input) - && isset($item->input['status']) - && $item->input['status'] == CommonITILObject::CLOSED) { + if ( + !$is_solution + && $conf->fields['require_solution_to_close' . $configSuffix] == 1 + && is_array($data) + && isset($data['status']) + && $data['status'] == CommonITILObject::CLOSED + ) { $solution = new ITILSolution(); $solutions = $solution->find([ 'itemtype' => $itemtype, - 'items_id' => $item->fields['id'], + 'items_id' => $data['id'], 'NOT' => [ 'status' => CommonITILValidation::REFUSED, ], @@ -411,9 +451,9 @@ public static function requireFieldsToClose(CommonDBTM $item): void $message = sprintf(__s('To close this %s, you must fill in the following fields:', 'moreoptions'), $itemTypeLabel) . '
' . $message; Session::addMessageAfterRedirect($message, false, ERROR); - $item->input = false; - return; + return false; } + return true; } public static function checkTaskRequirements(CommonDBTM $item): CommonDBTM diff --git a/tests/Units/ConfigTest.php b/tests/Units/ConfigTest.php index 7b29121..824d39b 100644 --- a/tests/Units/ConfigTest.php +++ b/tests/Units/ConfigTest.php @@ -306,6 +306,94 @@ public function testTicketMandatoryFieldsBeforeCloseTicket(): void $this->assertTrue($resetResult); } + /** + * Test mandatory fields before adding a solution + */ + public function testCannotAddSolutionWhenMissingMandatoryFields(): void + { + $this->login(); + + $conf = $this->getCurrentConfig(); + + // Configure mandatory fields before closing (which impacts solutions too) + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'require_technician_to_close_ticket' => 1, + 'require_category_to_close_ticket' => 1, + ]); + $this->assertTrue($result); + + // Create a ticket without mandatory fields + $ticket = $this->createItem( + \Ticket::class, + [ + 'name' => 'Test ticket solution', + 'content' => 'Test content', + ], + ); + $tid = $ticket->getID(); + + // Attempt to add a solution (Expected to fail because missing tech and category) + $solution = new \ITILSolution(); + $resultFields = $solution->add([ + 'itemtype' => \Ticket::class, + 'items_id' => $tid, + 'content' => 'My test solution', + 'status' => \CommonITILObject::SOLVED, + ]); + + $this->clearSessionMessages(); + $this->assertFalse($resultFields); + + // Add technician to the ticket + $user = new \User(); + $this->assertTrue($user->getFromDBByCrit(['name' => 'glpi'])); + + $this->createItem( + \Ticket_User::class, + [ + 'tickets_id' => $tid, + 'users_id' => $user->getID(), + 'type' => \Ticket_User::ASSIGN, + ], + ); + + // Create category and update ticket + $category = $this->createItem( + \ITILCategory::class, + [ + 'name' => 'Test category for solution test', + ], + ); + $this->updateItem( + \Ticket::class, + $tid, + [ + 'itilcategories_id' => $category->getID(), + ], + ); + + // Attempt to add solution with all mandatory fields present (Expected to succeed) + $solution2 = new \ITILSolution(); + $resultOk = $solution2->add([ + 'itemtype' => \Ticket::class, + 'items_id' => $tid, + 'solutiontypes_id' => 0, + 'content' => 'My test solution with fields ok', + 'status' => \CommonITILObject::SOLVED, + ]); + $this->assertIsInt($resultOk); + $this->clearSessionMessages(); + + // Reset config + $resetResult = $this->updateTestConfig($conf, [ + 'require_technician_to_close_ticket' => 0, + 'require_category_to_close_ticket' => 0, + ]); + $this->assertTrue($resetResult); + } + /** * Test mandatory fields before closing a change */