Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ dist/
vendor/
.gh_token
*.min.*
var/
.phpunit.result.cache
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed

- Fix warning : `creation of dynamic property $table is deprecated`
- Fix missing rights checks
- Fix unescaped output in order/reception forms

## [2.11.5] - 2026-03-24

Expand Down
13 changes: 13 additions & 0 deletions ajax/linkactions.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
*/

include("../../../inc/includes.php");

/** @var array $CFG_GLPI */
global $CFG_GLPI;

header("Content-Type: text/html; charset=UTF-8");
Html::header_nocache();
Session::checkLoginUser();
Expand Down Expand Up @@ -175,6 +179,15 @@
break;

case "check_unicity":
Session::checkRight('plugin_order_order', READ);

if (
!in_array($_POST['itemtype'] ?? '', $CFG_GLPI['asset_types'], true)
|| !in_array($_POST['field'] ?? '', ['name', 'serial'], true)
) {
throw new RuntimeException("Invalid item type or field");
}

$itemtype = new $_POST['itemtype']();
if (count($itemtype->find([$_POST['field'] => $_POST['field_value'], ])) > 0) {
echo "false";
Expand Down
2 changes: 2 additions & 0 deletions ajax/massreception.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
Html::header_nocache();

Session::checkLoginUser();
Session::checkRight('plugin_order_order', READ);
(new PluginOrderOrder())->check($_POST['plugin_order_orders_id'], READ);

$PluginOrderReception = new PluginOrderReception();

Expand Down
1 change: 1 addition & 0 deletions front/documentcategory.form.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

include('../../../inc/includes.php');
Session::checkLoginUser();
Session::checkRight("config", UPDATE);

$documentCategory = new PluginOrderDocumentCategory();

Expand Down
21 changes: 21 additions & 0 deletions front/order.form.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,15 @@
//Status update & order workflow
} else if (isset($_POST["validate"])) {
/* validate order */
$pluginOrderOrder->check($_POST["id"], UPDATE);
if (PluginOrderOrder::canView() && (PluginOrderOrder::canValidate() || !$config->useValidation())) {
$pluginOrderOrder->updateOrderStatus($_POST["id"], $config->getApprovedState(), $_POST["comment"]);
PluginOrderReception::updateDelivryStatus($_POST["id"]);
Session::addMessageAfterRedirect(__("Order is validated", "order"));
}
Html::back();
} else if (isset($_POST["waiting_for_approval"])) {
$pluginOrderOrder->check($_POST["id"], UPDATE);
if (PluginOrderOrder::canCreate()) {
$pluginOrderOrder->updateOrderStatus(
$_POST["id"],
Expand All @@ -105,6 +107,7 @@
}
Html::back();
} else if (isset($_POST["cancel_waiting_for_approval"])) {
$pluginOrderOrder->check($_POST["id"], UPDATE);
if (PluginOrderOrder::canView() && PluginOrderOrder::canCancel()) {
$pluginOrderOrder->updateOrderStatus(
$_POST["id"],
Expand All @@ -116,6 +119,7 @@

Html::back();
} else if (isset($_POST["cancel_order"])) {
$pluginOrderOrder->check($_POST["id"], UPDATE);
if (PluginOrderOrder::canView() && PluginOrderOrder::canCancel()) {
$pluginOrderOrder->updateOrderStatus(
$_POST["id"],
Expand All @@ -128,6 +132,7 @@

Html::back();
} else if (isset($_POST["undovalidation"])) {
$pluginOrderOrder->check($_POST["id"], UPDATE);
if (PluginOrderOrder::canView() && PluginOrderOrder::canUndo()) {
$pluginOrderOrder->updateOrderStatus(
$_POST["id"],
Expand All @@ -140,6 +145,7 @@
Html::back();
} else if (isset($_POST["add_item"])) {
//Details management
$pluginOrderOrder->check($_POST["plugin_order_orders_id"], UPDATE);
if ($_POST["discount"] < 0 || $_POST["discount"] > 100) {
Session::addMessageAfterRedirect(__("The discount pourcentage must be between 0 and 100", "order"), false, ERROR);
} else {
Expand Down Expand Up @@ -170,6 +176,7 @@
}
Html::back();
} else if (isset($_POST["delete_item"])) {
$pluginOrderOrder->check($_POST["plugin_order_orders_id"], UPDATE);
if (
isset($_POST["plugin_order_orders_id"])
&& ($_POST["plugin_order_orders_id"] > 0)
Expand Down Expand Up @@ -227,6 +234,11 @@
$new_value,
$_POST["plugin_order_orders_id"]
);

if (!$pluginOrderOrder_Item->belongsToOrder((int) $_POST["plugin_order_orders_id"])) {
continue;
}

$pluginOrderOrder_Item->delete(['id' => $ID]);
}
}
Expand All @@ -238,6 +250,7 @@
Html::back();
} else if (isset($_POST["add_itemfree"])) {
//Details management
$pluginOrderOrder->check($_POST["plugin_order_orders_id"], UPDATE);
if ($_POST["discount"] < 0 || $_POST["discount"] > 100) {
Session::addMessageAfterRedirect(__("The discount pourcentage must be between 0 and 100", "order"), false, ERROR);
} else {
Expand Down Expand Up @@ -313,6 +326,7 @@
}
Html::back();
} else if (isset($_POST["delete_itemfree"])) {
$pluginOrderOrder->check($_POST["plugin_order_orders_id"], UPDATE);
if (
isset($_POST["plugin_order_orders_id"])
&& ($_POST["plugin_order_orders_id"] > 0)
Expand Down Expand Up @@ -364,12 +378,18 @@
$new_value,
$_POST["plugin_order_orders_id"]
);

if (!$pluginOrderOrder_Item->belongsToOrder((int) $_POST["plugin_order_orders_id"])) {
continue;
}

$pluginOrderOrder_Item->delete(['id' => $ID]);
}
}
}
}
} else if (isset($_POST["update_item"])) {
$pluginOrderOrder->check($_POST["plugin_order_orders_id"], UPDATE);
if (isset($_POST['quantity'])) {
$pluginOrderOrder_Item->updateQuantity($_POST);
}
Expand Down Expand Up @@ -430,6 +450,7 @@

Html::back();
} else if (isset($_POST["update_detail_item"])) {
$pluginOrderOrder->check($_POST["plugin_order_orders_id"], UPDATE);
if (isset($_POST['detail_price_taxfree'])) {
foreach ($_POST['detail_price_taxfree'] as $item_id => $price) {
$pluginOrderOrder_Item->updatePrice_taxfree([
Expand Down
2 changes: 2 additions & 0 deletions front/order_item.form.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
$item = new PluginOrderOrder_Item();

if (isset($_POST['update'])) {
$item->getFromDB($_POST['id']);
(new PluginOrderOrder())->check($item->fields['plugin_order_orders_id'], UPDATE);
$item->update($_POST);
$item->updatePrices($_POST['id']);
Html::redirect($_SERVER['HTTP_REFERER']);
Expand Down
6 changes: 5 additions & 1 deletion front/reception.form.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@
$order_item = new PluginOrderOrder_Item();

if (isset($_POST["update"])) {
$order_item->getFromDB($_POST["id"]);
(new PluginOrderOrder())->check($order_item->fields["plugin_order_orders_id"], UPDATE);
if (PluginOrderReception::canCreate()) {
$order_item->getFromDB($_POST["id"]);
if ($order_item->fields["itemtype"] == 'SoftwareLicense') {
$result = $order_item->queryRef(
$order_item->fields["plugin_order_orders_id"],
Expand Down Expand Up @@ -82,6 +83,8 @@
]);
Html::redirect($_SERVER['HTTP_REFERER']);
} else if (isset($_POST["delete"])) {
$order_item->getFromDB($_POST["id"]);
(new PluginOrderOrder())->check($order_item->fields["plugin_order_orders_id"], UPDATE);
$reception->deleteDelivery($_POST["id"]);
$reception->updateReceptionStatus([
'items' => [
Expand All @@ -93,6 +96,7 @@
Html::redirect(Toolbox::getItemTypeFormURL('PluginOrderOrder') . "?id=" . $_POST["plugin_order_orders_id"]);
} else if (isset($_POST["bulk_reception"])) {
//Several new items are delivered
(new PluginOrderOrder())->check($_POST["plugin_order_orders_id"], UPDATE);
$reception->updateBulkReceptionStatus($_POST);
Html::redirect($_SERVER["HTTP_REFERER"]);
} else {
Expand Down
2 changes: 1 addition & 1 deletion hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ function plugin_order_uninstall()
'PluginOrderOrderType', 'PluginOrderOther', 'PluginOrderOtherType',
'PluginOrderPreference', 'PluginOrderProfile', 'PluginOrderReference_Supplier',
'PluginOrderSurveySupplier', 'PluginOrderDocumentCategory',
'PluginOrderAccountSection', 'PluginOrderAnalyticNature'
'PluginOrderAccountSection', 'PluginOrderAnalyticNature', 'PluginOrderReferenceFree'
];
foreach ($classes as $class) {
call_user_func([$class, 'uninstall']);
Expand Down
9 changes: 9 additions & 0 deletions inc/order.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -1818,6 +1818,15 @@ public function generateOrder($params)
throw new \RuntimeException("Invalid template name");
}

// Avoid access to another directory or to files that does not match allowed extension
$extensionPattern = '/\.(' . implode('|', array_map(fn($ext) => preg_quote((string) $ext, '/'), PLUGIN_ORDER_SIGNATURE_EXTENSION)) . ')$/';
if (
!empty($signature)
&& (preg_match('/[\\\\\/]/', $signature) !== 0 || preg_match($extensionPattern, $signature) === 0)
) {
throw new RuntimeException("Invalid signature file name");
}

$template_path = PLUGIN_ORDER_TEMPLATE_DIR . $template;

// Ensure the file exists and is readable
Expand Down
10 changes: 10 additions & 0 deletions inc/order_item.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,16 @@ public function getPricesATI($priceHT, $taxes)
return (!$priceHT ? 0 : $priceHT + (($priceHT * $taxes) / 100));
}

/**
* Check that this item belongs to the given order, to prevent cross-order IDOR on deletion.
*
* @param int $orders_id Order ID expected to own this item
*/
public function belongsToOrder(int $orders_id): bool
{
return (int) $this->fields['plugin_order_orders_id'] === $orders_id;
}

/**
* Calculate the total ecotax from all ordered items with their quantities
*
Expand Down
9 changes: 5 additions & 4 deletions inc/profile.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,11 @@ public static function migrateOneProfile($profiles_id)
}
break;
}
$query = "UPDATE `glpi_profilerights`
SET `rights`='" . $right . "'
WHERE `name`='$new' AND `profiles_id`='$profiles_id'";
$DB->query($query);
$DB->update(
'glpi_profilerights',
['rights' => $right],
['name' => $new, 'profiles_id' => $profiles_id],
);
}
}
}
Expand Down
13 changes: 7 additions & 6 deletions inc/referencefree.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,20 +107,21 @@ public static function install(Migration $migration)

public static function uninstall()
{
/** @var \DBmysql $DB */
/** @var DBmysql $DB */
global $DB;

$table = getTableForItemType(__CLASS__);
$table = getTableForItemType(self::class);
foreach (
["glpi_displaypreferences", "glpi_documents_items", "glpi_savedsearches",
"glpi_logs"
"glpi_logs",
] as $t
) {
$query = "DELETE FROM `$t` WHERE `itemtype`='" . __CLASS__ . "'";
$DB->query($query);
$itemtype = getItemTypeForTable($t);
$item = getItemForItemtype($itemtype);
$item->deleteByCriteria(['itemtype' => self::class]);
}

$DB->query("DROP TABLE IF EXISTS `$table`") or die($DB->error());
$DB->doQuery(sprintf('DROP TABLE IF EXISTS `%s`', $table));
}

/**
Expand Down
7 changes: 7 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<phpunit bootstrap="tests/bootstrap.php" colors="true" testdox="true">
<testsuites>
<testsuite name="Tests">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
62 changes: 62 additions & 0 deletions tests/Units/DocumentCategoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/**
* -------------------------------------------------------------------------
* Order plugin for GLPI
* -------------------------------------------------------------------------
*
* LICENSE
*
* This file is part of Order.
*
* Order 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.
*
* Order 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 Order. If not, see <http://www.gnu.org/licenses/>.
* -------------------------------------------------------------------------
* @copyright Copyright (C) 2009-2026 by Order plugin team.
* @license GPLv3 https://www.gnu.org/licenses/gpl-3.0.html
* @link https://github.com/pluginsGLPI/order
* -------------------------------------------------------------------------
*/

declare(strict_types=1);

namespace GlpiPlugin\Order\Tests\Units;

use DbTestCase;
use DocumentCategory;
use Glpi\Toolbox\Sanitizer;
use PluginOrderDocumentCategory;

final class DocumentCategoryTest extends DbTestCase
{
private const XSS_PAYLOAD = '<script>alert(1);</script>';

public function testShowForDocumentCategoryEscapesPrefix(): void
{
$document_category = $this->createItem(DocumentCategory::class, [
'name' => $this->getUniqueString(),
]);

$this->createItem(PluginOrderDocumentCategory::class, [
'documentcategories_id' => $document_category->getID(),
'documentcategories_prefix' => self::XSS_PAYLOAD,
]);

ob_start();
PluginOrderDocumentCategory::showForDocumentCategory($document_category);
$output = ob_get_clean();

$this->assertStringNotContainsString(self::XSS_PAYLOAD, $output);
$this->assertStringContainsString(Sanitizer::sanitize(self::XSS_PAYLOAD), $output);
}
}
Loading