Commit 96ce80ed authored by Johan Cwiklinski's avatar Johan Cwiklinski

Merge branch '9.5/bugfixes'

parents ef925f4d cd7b27d8
......@@ -53,6 +53,7 @@ The present file will list all changes made to the project; according to the
- Add datacenter items to global search
- Project task search options for Projects
- Automatic action to purge closed tickets
- Ability to automatically calculate project's percent done
### Changed
......
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 9.4.x | :heavy_check_mark: |
| 9.3.x | :x: |
| 9.2.x | :x: |
| < 9.2 | :x: |
## Reporting a Vulnerability
If you found a security issue, please contact us using the following email:
glpi-security AT ow2.org
Please **never** use standard issues to report security problems;
vulnerabilities are published once a fix release is available.
You should provide us all details about the issue and the way to reproduce it.
You may also provide a script that can be used to check the issue exists.
Once the report will be handled, and if the issue is not yet fixed (or in progress)
we'll add it to the GitHub security tab, and add you as observer. Meanwhile,
you will reserve a CVE for the issue.
......@@ -35,7 +35,7 @@ include ('../inc/includes.php');
header("Content-Type: text/html; charset=UTF-8");
Html::header_nocache();
echo Html::css("public/lib/prism.css");
echo Html::css("public/lib/prismjs.css");
echo Html::script("public/lib/prismjs.js");
$infos = Telemetry::getTelemetryInfos();
......
......@@ -54,10 +54,21 @@ if (!$item_device->canView()) {
if (isset($_POST["id"])) {
$_GET["id"] = $_POST["id"];
} else if (!isset($_GET["id"])) {
$_GET["id"] = -1;
$_GET["id"] = "";
}
if (isset($_POST["purge"])) {
if (isset($_POST["add"])) {
$item_device->check(-1, CREATE, $_POST);
if ($newID = $item_device->add($_POST)) {
Event::log($newID, get_class($item_device), 4, "setup",
sprintf(__('%1$s adds an item'), $_SESSION["glpiname"]));
if ($_SESSION['glpibackcreated']) {
Html::redirect($item_device->getLinkURL());
}
}
Html::back();
} else if (isset($_POST["purge"])) {
$item_device->check($_POST["id"], PURGE);
$item_device->delete($_POST, 1);
......@@ -78,7 +89,12 @@ if (isset($_POST["purge"])) {
Html::back();
} else {
Html::header($item_device->getTypeName(Session::getPluralNumber()), '', "config", "commondevice", $item_device->getDeviceType());
if (in_array($item_device->getType(), $CFG_GLPI['devices_in_menu'])) {
Html::header($item_device->getTypeName(Session::getPluralNumber()), $_SERVER['PHP_SELF'], "assets", strtolower($item_device->getType()));
} else {
Html::header($item_device->getTypeName(Session::getPluralNumber()), '', "config", "commondevice", $item_device->getDeviceType());
}
if (!isset($options)) {
$options = [];
......
......@@ -44,7 +44,11 @@ if (!$itemDevice->canView()) {
Html::displayRightError();
}
Html::header($itemDevice->getTypeName(Session::getPluralNumber()), '', "config", "commondevice", $itemDevice->getDeviceType());
if (in_array($itemDevice->getType(), $CFG_GLPI['devices_in_menu'])) {
Html::header($itemDevice->getTypeName(Session::getPluralNumber()), $_SERVER['PHP_SELF'], "assets", strtolower($itemDevice->getType()));
} else {
Html::header($itemDevice->getTypeName(Session::getPluralNumber()), '', "config", "commondevice", $itemDevice->getDeviceType());
}
Search::show($_GET['itemtype']);
......
......@@ -228,6 +228,11 @@ if (isset($_GET["id"]) && ($_GET["id"] > 0)) {
}
} else {
if (Session::getCurrentInterface() != 'central') {
Html::redirect($CFG_GLPI["root_doc"]."/front/helpdesk.public.php?create_ticket=1");
die;
}
Html::header(__('New ticket'), '', "helpdesk", "ticket");
unset($_REQUEST['id']);
unset($_GET['id']);
......
......@@ -2279,7 +2279,7 @@ abstract class API extends CommonGLPI {
public function inlineDocumentation($file) {
//this should be served from a slim route
$this->header(true, __("API Documentation"));
echo Html::css("public/lib/prism.css");
echo Html::css("public/lib/prismjs.css");
echo Html::script("public/lib/prismjs.js");
echo "<div class='documentation'>";
......
......@@ -154,18 +154,6 @@ class ChangeTask extends CommonITILTask {
}
/**
* Display a Planning Item
*
* @param array $val Array of the item to display
*
* @return string Already planned information
**/
static function getAlreadyPlannedInformation($val) {
return parent::genericGetAlreadyPlannedInformation(__CLASS__, $val);
}
/**
* Display a Planning Item
*
......
......@@ -5172,6 +5172,41 @@ class CommonDBTM extends CommonGLPI {
}
}
/**
* Ensure the relation would not create a circular parent-child relation.
* @since 9.5.0
* @param int $items_id The ID of the item to evaluate.
* @param int $parents_id The wanted parent of the specified item.
* @return bool True if there is a circular relation.
*/
static function checkCircularRelation($items_id, $parents_id) {
global $DB;
$fk = static::getForeignKeyField();
if ($items_id == 0 || $parents_id == 0 || !$DB->fieldExists(static::getTable(), $fk)) {
return false;
}
$next_parent = $parents_id;
while ($next_parent > 0) {
if ($next_parent == $items_id) {
// This item is a parent higher up
return true;
}
$iterator = $DB->request([
'SELECT' => [$fk],
'FROM' => static::getTable(),
'WHERE' => ['id' => $next_parent]
]);
if ($iterator->count()) {
$next_parent = $iterator->next()[$fk];
} else {
// Invalid parent
return false;
}
}
// No circular relations
return false;
}
/**
* Dispatch item event.
*
......
......@@ -204,7 +204,17 @@ abstract class CommonITILObject extends CommonDBTM {
* @return boolean
*/
function canAddFollowups() {
return Session::haveRight(static::$rightname, UPDATE) and Session::haveRight('followup', CREATE);
return ((Session::haveRight("followup", ITILFollowup::ADDMYTICKET)
&& ($this->isUser(CommonITILActor::REQUESTER, Session::getLoginUserID())
|| (isset($this->fields["users_id_recipient"])
&& ($this->fields["users_id_recipient"] === Session::getLoginUserID()))))
|| Session::haveRight('followup', ITILFollowup::ADDALLTICKET)
|| (Session::haveRight('followup', ITILFollowup::ADDGROUPTICKET)
&& isset($_SESSION["glpigroups"])
&& $this->haveAGroup(CommonITILActor::REQUESTER, $_SESSION['glpigroups']))
|| $this->isUser(CommonITILActor::ASSIGN, Session::getLoginUserID())
|| (isset($_SESSION["glpigroups"])
&& $this->haveAGroup(CommonITILActor::ASSIGN, $_SESSION['glpigroups'])));
}
......
......@@ -36,6 +36,7 @@ if (!defined('GLPI_ROOT')) {
/// TODO extends it from CommonDBChild
abstract class CommonITILTask extends CommonDBTM {
use PlanningEvent;
// From CommonDBTM
public $auto_message_on_action = false;
......@@ -49,7 +50,7 @@ abstract class CommonITILTask extends CommonDBTM {
function getItilObjectItemType() {
public function getItilObjectItemType() {
return str_replace('Task', '', $this->getType());
}
......@@ -1149,31 +1150,6 @@ abstract class CommonITILTask extends CommonDBTM {
}
/**
* Display a Planning Item
*
* @param string $itemtype itemtype
* @param array $val the item to display
*
* @return string Output
**/
static function genericGetAlreadyPlannedInformation($itemtype, array $val) {
if ($item = getItemForItemtype($itemtype)) {
$objectitemtype = $item->getItilObjectItemType();
//TRANS: %1$s is a type, %2$$ is a date, %3$s is a date
$out = sprintf(__('%1$s: from %2$s to %3$s:'), $item->getTypeName(1),
Html::convDateTime($val["begin"]), Html::convDateTime($val["end"]));
$out .= "<br><a href='".$objectitemtype::getFormURLWithID($val[getForeignKeyFieldForItemType($objectitemtype)])
."&amp;forcetab=".$itemtype."$1'>";
$out .= Html::resume_text($val["name"], 80).'</a>';
return $out;
}
}
/**
* Display a Planning Item
*
......
......@@ -480,7 +480,12 @@ abstract class CommonTreeDropdown extends CommonDropdown {
$ID = $this->getID();
$this->check($ID, READ);
$fields = $this->getAdditionalFields();
$fields = array_filter(
$this->getAdditionalFields(),
function ($field) {
return isset($field['list']) && $field['list'];
}
);
$nb = count($fields);
$entity_assign = $this->isEntityAssign();
......@@ -522,9 +527,7 @@ abstract class CommonTreeDropdown extends CommonDropdown {
$header .= "<th>".__('Entity')."</th>";
}
foreach ($fields as $field) {
if ($field['list']) {
$header .= "<th>".$field['label']."</th>";
}
$header .= "<th>".$field['label']."</th>";
}
$header .= "<th>".__('Comments')."</th>";
$header .= "</tr>\n";
......@@ -558,27 +561,25 @@ abstract class CommonTreeDropdown extends CommonDropdown {
}
foreach ($fields as $field) {
if ($field['list']) {
echo "<td>";
switch ($field['type']) {
case 'UserDropdown' :
echo getUserName($data[$field['name']]);
break;
case 'bool' :
echo Dropdown::getYesNo($data[$field['name']]);
break;
case 'dropdownValue' :
echo Dropdown::getDropdownName(getTableNameForForeignKeyField($field['name']),
$data[$field['name']]);
break;
default:
echo $data[$field['name']];
}
echo "</td>";
echo "<td>";
switch ($field['type']) {
case 'UserDropdown' :
echo getUserName($data[$field['name']]);
break;
case 'bool' :
echo Dropdown::getYesNo($data[$field['name']]);
break;
case 'dropdownValue' :
echo Dropdown::getDropdownName(getTableNameForForeignKeyField($field['name']),
$data[$field['name']]);
break;
default:
echo $data[$field['name']];
}
echo "</td>";
}
echo "<td>".$data['comment']."</td>";
echo "</tr>\n";
......
......@@ -239,6 +239,12 @@ class Config extends CommonDBTM {
}
}
if (isset($input['_update_devices_in_menu'])) {
$input['devices_in_menu'] = exportArrayToDB(
(isset($input['devices_in_menu']) ? $input['devices_in_menu'] : [])
);
}
// lock mechanism update
if (isset( $input['lock_use_lock_item'])) {
$input['lock_item_list'] = exportArrayToDB((isset($input['lock_item_list'])
......@@ -546,6 +552,34 @@ class Config extends CommonDBTM {
$rand);
echo "</td></tr>";
echo "<tr class='tab_bg_2'>";
echo "<td><label for='devices_in_menu$rand'>".__('Devices displayed in menu')."</label></td>";
echo "<td>";
$dd_params = [
'name' => 'devices_in_menu',
'values' => $CFG_GLPI['devices_in_menu'],
'display' => true,
'rand' => $rand,
'multiple' => true,
'size' => 3
];
$item_devices_types = [];
foreach ($CFG_GLPI['itemdevices'] as $key => $itemtype) {
if ($item = getItemForItemtype($itemtype)) {
$item_devices_types[$itemtype] = $item->getTypeName();
} else {
unset($CFG_GLPI['itemdevices'][$key]);
}
}
Dropdown::showFromArray($dd_params['name'], $item_devices_types, $dd_params);
echo "<input type='hidden' name='_update_devices_in_menu' value='1'>";
echo "</td>";
echo "</tr>\n";
echo "</table>";
if (Session::haveRightsOr("transfer", [CREATE, UPDATE])
......@@ -2884,6 +2918,10 @@ class Config extends CommonDBTM {
$CFG_GLPI['priority_matrix'] = importArrayFromDB($CFG_GLPI['priority_matrix']);
}
if (isset($CFG_GLPI['devices_in_menu'])) {
$CFG_GLPI['devices_in_menu'] = importArrayFromDB($CFG_GLPI['devices_in_menu']);
}
if (isset($CFG_GLPI['lock_item_list'])) {
$CFG_GLPI['lock_item_list'] = importArrayFromDB($CFG_GLPI['lock_item_list']);
}
......
......@@ -198,7 +198,6 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
$db_hostport = $db_host . (!empty($db_port) ? ':' . $db_port : '');
$reconfigure = $input->getOption('reconfigure');
$no_interaction = $input->getOption('no-interaction'); // Base symfony/console option
if (file_exists(GLPI_CONFIG_DIR . '/db.yaml') && !$reconfigure) {
// Prevent overriding of existing DB
......@@ -208,43 +207,22 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
return self::ERROR_DB_CONFIG_ALREADY_SET;
}
if (empty($db_name)) {
throw new InvalidArgumentException(
__('Database name defined by --db-name option cannot be empty.')
);
}
if (null === $db_pass) {
// Will be null if option used without value and without interaction
throw new InvalidArgumentException(
__('--db-password option value cannot be null.')
);
}
if (!$no_interaction) {
// Ask for confirmation (unless --no-interaction)
$informations = new Table($output);
$this->validateConfigInput($input);
$informations->addRow([__('Database driver'), $db_driver]);
$informations->addRow([__('Database host'), $db_hostport]);
$informations->addRow([__('Database name'), $db_name]);
$informations->addRow([__('Database user'), $db_user]);
$informations->render();
/** @var Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$run = $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(__('Do you want to continue ?') . ' [Yes/no]', true)
$run = $this->askForDbConfigConfirmation(
$input,
$output,
$db_hostport,
$db_name,
$db_user
);
if (!$run) {
$output->writeln(
'<comment>' . __('Configuration aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
if (!$run) {
$output->writeln(
'<comment>' . __('Configuration aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
return self::ABORTED_BY_USER;
}
return self::ABORTED_BY_USER;
}
try {
......@@ -301,4 +279,82 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
return true;
}
/**
* Check if DB is already configured.
*
* @return boolean
*/
protected function isDbAlreadyConfigured() {
return file_exists(GLPI_CONFIG_DIR . '/config_db.php');
}
/**
* Validate configuration variables from input.
*
* @param InputInterface $input
*
* @throws InvalidArgumentException
*/
protected function validateConfigInput(InputInterface $input) {
$db_name = $input->getOption('db-name');
$db_user = $input->getOption('db-user');
$db_pass = $input->getOption('db-password');
if (empty($db_name)) {
throw new InvalidArgumentException(
__('Database name defined by --db-name option cannot be empty.')
);
}
if (empty($db_user)) {
throw new InvalidArgumentException(
__('Database user defined by --db-user option cannot be empty.')
);
}
if (null === $db_pass) {
// Will be null if option used without value and without interaction
throw new InvalidArgumentException(
__('--db-password option value cannot be null.')
);
}
}
/**
* Ask user to confirm DB configuration.
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @return boolean
*/
protected function askForDbConfigConfirmation(
InputInterface $input,
OutputInterface $output,
$db_hostport,
$db_name,
$db_user) {
$informations = new Table($output);
$informations->addRow([__('Database host'), $db_hostport]);
$informations->addRow([__('Database name'), $db_name]);
$informations->addRow([__('Database user'), $db_user]);
$informations->render();
if ($input->getOption('no-interaction')) {
// Consider that config is validated if user require no interaction
return true;
}
/** @var Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
return $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(__('Do you want to continue ?') . ' [Yes/no]', true)
);
}
}
......@@ -39,10 +39,11 @@ if (!defined('GLPI_ROOT')) {
use Glpi\DatabaseFactory;
use Glpi\Application\LocalConfigurationManager;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Yaml\Yaml;
use Toolbox;
......@@ -109,18 +110,46 @@ class InstallCommand extends AbstractConfigureCommand {
protected function interact(InputInterface $input, OutputInterface $output) {
if ($this->shouldSetDBConfig($input, $output)) {
if ($this->isDbAlreadyConfigured()
&& $this->isInputContainingConfigValues($input, $output)
&& !$input->getOption('reconfigure')) {
/** @var Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$reconfigure = $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(
__('Command input contains configuration options that may override existing configuration.')
. PHP_EOL
. __('Do you want to reconfigure database ?') . ' [Yes/no]',
true
)
);
$input->setOption('reconfigure', $reconfigure);
}
if (!$this->isDbAlreadyConfigured() || $input->getOption('reconfigure')) {
parent::interact($input, $output);
}
}
protected function execute(InputInterface $input, OutputInterface $output) {
$db_name = $input->getOption('db-name');
$default_language = $input->getOption('default-language');
$force = $input->getOption('force');
if ($this->shouldSetDBConfig($input, $output)) {
if ($this->isDbAlreadyConfigured()
&& $this->isInputContainingConfigValues($input, $output)
&& !$input->getOption('reconfigure')) {
// Prevent overriding of existing DB when input contains configuration values and
// --reconfigure option is not used.
$output->writeln(
'<error>' . __('Database configuration already exists. Use --reconfigure option to override existing configuration.') . '</error>'
);
return self::ERROR_DB_CONFIG_ALREADY_SET;
}
if (!$this->isDbAlreadyConfigured() || $input->getOption('reconfigure')) {
$result = $this->configureDatabase($input, $output);
if (self::ABORTED_BY_USER === $result) {
......@@ -128,7 +157,49 @@ class InstallCommand extends AbstractConfigureCommand {
} else if (self::SUCCESS !== $result) {
return $result; // Fail with error code
}
$db_host = $input->getOption('db-host');
$db_port = $input->getOption('db-port');
$db_hostport = $db_host . (!empty($db_port) ? ':' . $db_port : '');
$db_name = $input->getOption('db-name');
$db_user = $input->getOption('db-user');
$db_pass = $input->getOption('db-password');
}
} else {
// Ask to confirm installation based on existing configuration.
$run = $this->askForDbConfigConfirmation(
$input,
$output,
$DB->dbhost,
$DB->dbdefault,
$DB->dbuser
);
if (!$run) {
$output->writeln(
'<comment>' . __('Installation aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
return 0;
}
// $DB->dbhost can be array when using round robin feature
$hostport = explode(':', is_array($DB->dbhost) ? $DB->dbhost[0] : $DB->dbhost);
$db_host = $hostport[0];
if (count($hostport) < 2) {
// Host only case
$db_port = null;
} else if (intval($hostport[1]) > 0) {
// Host:port case
$db_port = $hostport[1];
} else {
// :Socket case
// TODO Handle socket connection
throw new \UnexpectedValueException('DB connection through socket is not yet handled in installation command');
}
$db_name = $DB->dbdefault;
$db_user = $DB->dbuser;
$db_pass = rawurldecode($DB->dbpassword); //rawurldecode as in DBmysql::connect()
$dbh = DatabaseFactory::create();
......@@ -231,4 +302,33 @@ class InstallCommand extends AbstractConfigureCommand {
return $input->getOption('reconfigure') || !file_exists(GLPI_CONFIG_DIR . '/config_db.php');
}
/**
* Check if input contains DB config options.
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @return boolean
*/
private function isInputContainingConfigValues(InputInterface $input, OutputInterface $output) {
$config_options = [
'db-host',
'db-port',
'db-name',
'db-user',
'db-password',
];
foreach ($config_options as $option) {
$default_value = $this->getDefinition()->getOption($option)->getDefault();
$input_value = $input->getOption($option);
if ($default_value !== $input_value) {
return true;
}
}
return false;
}
}
......@@ -58,6 +58,7 @@ class DCRoom extends CommonDBTM {
->addStandardTab('Infocom', $ong, $options)
->addStandardTab('Contract_Item', $ong, $options)
->addStandardTab('Document_Item', $ong, $options)
->addStandardTab('Link', $ong, $options)
->addStandardTab('Ticket', $ong, $options)
->addStandardTab('Item_Problem', $ong, $options)
->addStandardTab('Change_Item', $ong, $options)
......
......@@ -242,21 +242,6 @@ $CFG_GLPI["document_types"] = ['Budget', 'CartridgeItem', 'Change'
$CFG_GLPI["consumables_types"] = ['Group', 'User'];
$CFG_GLPI["itemdevices"] = ['Item_DevicePowerSupply', 'Item_DevicePci',
'Item_DeviceCase', 'Item_DeviceGraphicCard',
'Item_DeviceMotherBoard', 'Item_DeviceNetworkCard',
'Item_DeviceSoundCard', 'Item_DeviceControl',
'Item_DeviceHardDrive', 'Item_DeviceDrive', 'Item_DeviceMemory',
'Item_DeviceProcessor', 'Item_DeviceGeneric',
'Item_DeviceBattery', 'Item_DeviceFirmware', 'Item_DeviceSimcard',
'Item_DeviceSensor'];
$CFG_GLPI["contract_types"] = array_merge(['Computer', 'Monitor', 'NetworkEquipment',
'Peripheral', 'Phone', 'Printer', 'Project', 'Line',
'Software', 'SoftwareLicense', 'Certificate',
'DCRoom', 'Rack', 'Enclosure', 'PDU', 'Cluster'],
$CFG_GLPI['itemdevices']);
$CFG_GLPI["report_types"] = ['Computer', 'Monitor', 'NetworkEquipment',
'Peripheral', 'Phone', 'Printer', 'Project',
'Software', 'SoftwareLicense', 'Certificate'];