mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-18 18:04:11 +01:00
Compare commits
3 Commits
feature/91
...
feature/85
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3000cc024 | ||
|
|
6e48b07e36 | ||
|
|
e042717fa4 |
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -9,7 +9,7 @@ Any PRs not following the guidelines or with missing information will not be con
|
||||
## Base information
|
||||
| Question | Answer
|
||||
|---------------------------------------------------------------|--------
|
||||
| Related to a SourceForge thread / Another PR / Combodo ticket? | <!-- Put the URL -->
|
||||
| Related to a SourceForge thead / Another PR / Combodo ticket? | <!-- Put the URL -->
|
||||
| Type of change? | Bug fix / Enhancement / Translations
|
||||
|
||||
|
||||
|
||||
@@ -536,7 +536,6 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
// Cache
|
||||
$this->m_aObjectActionGrants = [];
|
||||
$this->m_aAdministrators = null;
|
||||
$this->aUsersProfilesList = [];
|
||||
}
|
||||
|
||||
public function LoadCache()
|
||||
|
||||
@@ -16,5 +16,5 @@ require_once(APPROOT.'/application/audit.category.class.inc.php');
|
||||
require_once(APPROOT.'/application/audit.domain.class.inc.php');
|
||||
require_once(APPROOT.'/application/audit.rule.class.inc.php');
|
||||
require_once(APPROOT.'/application/query.class.inc.php');
|
||||
require_once(APPROOT.'/setup/moduleinstallation/moduleinstallation.class.inc.php');
|
||||
require_once(APPROOT.'/setup/moduleinstallation.class.inc.php');
|
||||
require_once(APPROOT.'/application/utils.inc.php');
|
||||
|
||||
@@ -48,7 +48,7 @@ class AuditCategory extends cmdbAbstractObject
|
||||
MetaModel::Init_AddAttribute(new AttributeString("name", ["description" => "Short name for this category", "allowed_values" => null, "sql" => "name", "default_value" => "", "is_null_allowed" => false, "depends_on" => []]));
|
||||
MetaModel::Init_AddAttribute(new AttributeString("description", ["allowed_values" => null, "sql" => "description", "default_value" => "", "is_null_allowed" => true, "depends_on" => []]));
|
||||
MetaModel::Init_AddAttribute(new AttributeOQL("definition_set", ["allowed_values" => null, "sql" => "definition_set", "default_value" => "", "is_null_allowed" => false, "depends_on" => []]));
|
||||
MetaModel::Init_AddAttribute(new AttributeLinkedSet("rules_list", ["linked_class" => "AuditRule", "ext_key_to_me" => "category_id", "allowed_values" => null, "count_min" => 0, "count_max" => 0, "depends_on" => [], "edit_mode" => LINKSET_EDITMODE_INPLACE, "edit_when" => LINKSET_EDITWHEN_ALWAYS, "tracking_level" => LINKSET_TRACKING_ALL]));
|
||||
MetaModel::Init_AddAttribute(new AttributeLinkedSet("rules_list", ["linked_class" => "AuditRule", "ext_key_to_me" => "category_id", "allowed_values" => null, "count_min" => 0, "count_max" => 0, "depends_on" => [], "edit_mode" => LINKSET_EDITMODE_INPLACE, "tracking_level" => LINKSET_TRACKING_ALL]));
|
||||
MetaModel::Init_AddAttribute(new AttributeInteger("ok_error_tolerance", ["allowed_values" => null, "sql" => "ok_error_tolerance", "default_value" => 5, "is_null_allowed" => true, "depends_on" => []]));
|
||||
MetaModel::Init_AddAttribute(new AttributeInteger("warning_error_tolerance", ["allowed_values" => null, "sql" => "warning_error_tolerance", "default_value" => 25, "is_null_allowed" => true, "depends_on" => []]));
|
||||
MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect(
|
||||
|
||||
@@ -52,14 +52,13 @@ class AuditRule extends cmdbAbstractObject
|
||||
MetaModel::Init_AddAttribute(new AttributeEnum("valid_flag", ["allowed_values" => new ValueSetEnum('true,false'), "sql" => "valid_flag", "default_value" => "true", "is_null_allowed" => false, "depends_on" => []]));
|
||||
MetaModel::Init_AddAttribute(new AttributeExternalKey("category_id", ["allowed_values" => null, "sql" => "category_id", "targetclass" => "AuditCategory", "is_null_allowed" => false, "on_target_delete" => DEL_MANUAL, "depends_on" => []]));
|
||||
MetaModel::Init_AddAttribute(new AttributeExternalField("category_name", ["allowed_values" => null, "extkey_attcode" => 'category_id', "target_attcode" => "name"]));
|
||||
MetaModel::Init_AddAttribute(new AttributeExternalKey("contact_id", ["allowed_values" => null, "sql" => "contact_id", "targetclass" => "Contact", "is_null_allowed" => true, "on_target_delete" => DEL_MANUAL, "depends_on" => []]));
|
||||
MetaModel::Init_AddAttribute(new AttributeHTML("process", ["allowed_values" => null, "sql" => "process", "default_value" => "", "is_null_allowed" => true, "depends_on" => []]));
|
||||
|
||||
// Display lists
|
||||
MetaModel::Init_SetZListItems('details', ['category_id', 'name', 'description', 'query', 'valid_flag', 'process', 'contact_id']); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', ['category_id', 'description', 'query']); // Attributes to be displayed for a list
|
||||
MetaModel::Init_SetZListItems('details', ['category_id', 'name', 'description', 'query', 'valid_flag']); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', ['category_id', 'description', 'valid_flag']); // Attributes to be displayed for a list
|
||||
// Search criteria
|
||||
MetaModel::Init_SetZListItems('standard_search', ['category_id', 'name', 'description', 'valid_flag', 'query']); // Criteria of the std search form
|
||||
MetaModel::Init_SetZListItems('default_search', ['name', 'description', 'category_id', 'contact_id', 'query']); // Criteria of the advanced search form
|
||||
MetaModel::Init_SetZListItems('default_search', ['name', 'description', 'category_id']); // Criteria of the advanced search form
|
||||
}
|
||||
|
||||
public static function GetShortcutActions($sFinalClass)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
@@ -10,4 +9,5 @@
|
||||
*/
|
||||
class CoreOqlException extends CoreException
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
@@ -10,4 +9,5 @@
|
||||
*/
|
||||
class CoreOqlMultipleResultsForbiddenException extends CoreOqlException
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ MetaModel::IncludeModule('application/user.dashboard.class.inc.php');
|
||||
MetaModel::IncludeModule('application/audit.rule.class.inc.php');
|
||||
MetaModel::IncludeModule('application/audit.domain.class.inc.php');
|
||||
MetaModel::IncludeModule('application/query.class.inc.php');
|
||||
MetaModel::IncludeModule('setup/moduleinstallation/moduleinstallation.class.inc.php');
|
||||
MetaModel::IncludeModule('setup/moduleinstallation.class.inc.php');
|
||||
|
||||
MetaModel::IncludeModule('core/event.class.inc.php');
|
||||
MetaModel::IncludeModule('core/action.class.inc.php');
|
||||
|
||||
@@ -211,14 +211,6 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => true,
|
||||
],
|
||||
'allowed_login_types' => [
|
||||
'type' => 'string',
|
||||
'description' => 'List of login types allowed (separated by | ): form, external, basic, token',
|
||||
'default' => DEFAULT_ALLOWED_LOGIN_TYPES,
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => true,
|
||||
],
|
||||
'app_icon_url' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Hyperlink to redirect the user when clicking on the application icon (in the main window, or login/logoff pages)',
|
||||
@@ -2418,7 +2410,6 @@ class Config
|
||||
public function SetAllowedLoginTypes($aAllowedLoginTypes)
|
||||
{
|
||||
$this->m_sAllowedLoginTypes = implode('|', $aAllowedLoginTypes);
|
||||
$this->Set('allowed_login_types', implode('|', $aAllowedLoginTypes));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2685,13 +2676,14 @@ class Config
|
||||
*
|
||||
* @param array $aParamValues
|
||||
* @param ?string $sModulesDir
|
||||
* @param bool $bPreserveModuleSettings
|
||||
*
|
||||
* @return void The current object is modified directly
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public function UpdateFromParams($aParamValues, $sModulesDir = null)
|
||||
public function UpdateFromParams($aParamValues, $sModulesDir = null, $bPreserveModuleSettings = false)
|
||||
{
|
||||
if (isset($aParamValues['application_path'])) {
|
||||
$this->Set('app_root_url', $aParamValues['application_path']);
|
||||
@@ -2739,10 +2731,7 @@ class Config
|
||||
} else {
|
||||
$aSelectedModules = null;
|
||||
}
|
||||
|
||||
if (! is_null($sModulesDir)) {
|
||||
$this->UpdateIncludes($sModulesDir, $aSelectedModules);
|
||||
}
|
||||
$this->UpdateIncludes($sModulesDir, $aSelectedModules);
|
||||
|
||||
if (isset($aParamValues['source_dir'])) {
|
||||
$this->Set('source_dir', $aParamValues['source_dir']);
|
||||
@@ -2760,13 +2749,17 @@ class Config
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function UpdateIncludes(string $sModulesDir, $aSelectedModules = null)
|
||||
public function UpdateIncludes($sModulesDir, $aSelectedModules = null)
|
||||
{
|
||||
if ($sModulesDir === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the arrays below with default values for the application...
|
||||
$oEmptyConfig = new Config('dummy_file', false); // Do NOT load any config file, just set the default values
|
||||
$aAddOns = $oEmptyConfig->GetAddOns();
|
||||
|
||||
$aModules = ModuleDiscovery::GetModulesOrderedByDependencies([APPROOT.$sModulesDir]);
|
||||
$aModules = ModuleDiscovery::GetAvailableModules([APPROOT.$sModulesDir]);
|
||||
foreach ($aModules as $sModuleId => $aModuleInfo) {
|
||||
list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
|
||||
if (is_null($aSelectedModules) || in_array($sModuleName, $aSelectedModules)) {
|
||||
|
||||
@@ -71,9 +71,12 @@ class DesignDocument extends DOMDocument
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @param string $source
|
||||
* @param int $options
|
||||
*
|
||||
* @return bool|\DOMDocument
|
||||
*/
|
||||
public function loadXML(string $source, int $options = 0): bool
|
||||
public function loadXML(string $source, int $options = 0): bool|DOMDocument
|
||||
{
|
||||
return parent::loadXML($source, $options | LIBXML_BIGLINES);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ use Combodo\iTop\Application\EventRegister\ApplicationEvents;
|
||||
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Service\Events\EventService;
|
||||
use Combodo\iTop\Setup\ModuleDependency\Module;
|
||||
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
|
||||
|
||||
require_once APPROOT.'core/modulehandler.class.inc.php';
|
||||
require_once APPROOT.'core/querymodifier.class.inc.php';
|
||||
@@ -464,43 +462,6 @@ abstract class MetaModel
|
||||
return call_user_func([$sClass, 'GetClassDescription'], $sClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sClass
|
||||
*
|
||||
* @return string
|
||||
* @throws \CoreException
|
||||
*/
|
||||
final public static function GetModuleName($sClass)
|
||||
{
|
||||
try {
|
||||
$oReflectionClass = new ReflectionClass($sClass);
|
||||
$sDir = realpath(dirname($oReflectionClass->getFileName()));
|
||||
$sApproot = realpath(APPROOT);
|
||||
while (($sDir !== $sApproot) && (str_contains($sDir, $sApproot))) {
|
||||
$aFiles = glob("$sDir/module.*.php");
|
||||
if (count($aFiles) > 1) {
|
||||
return 'core';
|
||||
}
|
||||
|
||||
if (count($aFiles) == 0) {
|
||||
$sDir = realpath(dirname($sDir));
|
||||
continue;
|
||||
}
|
||||
|
||||
$sModuleFilePath = $aFiles[0];
|
||||
$aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath);
|
||||
$sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID];
|
||||
list($sModuleName, ) = ModuleDiscovery::GetModuleName($sModuleId);
|
||||
|
||||
return $sModuleName;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw new CoreException("Cannot find class module", ['class' => $sClass], '', $e);
|
||||
}
|
||||
|
||||
return 'core';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sClass
|
||||
*
|
||||
|
||||
@@ -404,14 +404,14 @@ abstract class User extends cmdbAbstractObject
|
||||
}
|
||||
|
||||
if (!in_array(ADMIN_PROFILE_NAME, $aProfiles)) {
|
||||
// Prevent a User to lose the right to modify Users
|
||||
// Check if the user is yet allowed to modify Users
|
||||
if (method_exists($oAddon, 'ResetCache')) {
|
||||
$aCurrentProfiles = Session::Get('profile_list');
|
||||
// Set the current profiles into a session variable (not yet in the database)
|
||||
Session::Set('profile_list', $aProfiles);
|
||||
|
||||
$oAddon->ResetCache();
|
||||
if (!$oAddon->IsActionAllowed($this, get_class($this), UR_ACTION_MODIFY, null)) {
|
||||
if (!$oAddon->IsActionAllowed($this, 'User', UR_ACTION_MODIFY, null)) {
|
||||
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:CurrentProfilesHaveInsufficientRights');
|
||||
}
|
||||
$oAddon->ResetCache();
|
||||
@@ -422,19 +422,6 @@ abstract class User extends cmdbAbstractObject
|
||||
Session::Set('profile_list', $aCurrentProfiles);
|
||||
}
|
||||
}
|
||||
// Prevent an administrator to remove their own admin profile
|
||||
if (UserRights::IsAdministrator($this)) {
|
||||
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:AdminProfileCannotBeRemovedBySelf');
|
||||
}
|
||||
}
|
||||
} elseif ($this->IsPrivilegedUser()) {
|
||||
// Prevent Privileged User to be saved with profiles denying the access to the backoffice
|
||||
$oSet->Rewind();
|
||||
while ($oUserProfile = $oSet->Fetch()) {
|
||||
$sProfile = $oUserProfile->Get('profile');
|
||||
if (in_array($sProfile, $aForbiddenProfiles)) {
|
||||
$this->m_aCheckIssues[] = Dict::Format('Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice', $sProfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -648,21 +635,6 @@ abstract class User extends cmdbAbstractObject
|
||||
}
|
||||
return UserRights::GetUserId() == $this->GetKey();
|
||||
}
|
||||
|
||||
private function IsPrivilegedUser(): bool
|
||||
{
|
||||
$aPrivilegedProfiles = ['Administrator' => '1', 'REST Services User' => '1024', 'SuperUser' => '117'];
|
||||
|
||||
$oSet = $this->Get('profile_list');
|
||||
$oSet->Rewind();
|
||||
while ($oUserProfile = $oSet->Fetch()) {
|
||||
$iProfile = $oUserProfile->Get('profileid');
|
||||
if (in_array($iProfile, $aPrivilegedProfiles)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -311,35 +311,29 @@ fieldset {
|
||||
}
|
||||
|
||||
.module-selection-body {
|
||||
overflow: auto;
|
||||
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, .06) !important;
|
||||
background-color: #F7FAFC;
|
||||
padding: 10px;
|
||||
.wiz-choice{
|
||||
&:checked ~ .description {
|
||||
#itop-ticket-mgmt-simple-ticket-enhanced-portal:not(:checked),
|
||||
#itop-ticket-mgmt-itil-enhanced-portal:not(:checked) {
|
||||
~ .description::after {
|
||||
content: "Legacy portal is no longer part of iTop, by leaving this option unchecked your portal users won't be able to access iTop anymore.";
|
||||
display: block;
|
||||
margin-top: 0.5em;
|
||||
font-weight: bold;
|
||||
color: $legacy-portal-removal-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(:checked) ~ label .setup-extension-tag.checked{
|
||||
display:none;
|
||||
}
|
||||
&:checked ~ label .setup-extension-tag.unchecked{
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
overflow: auto;
|
||||
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, .06) !important;
|
||||
background-color: #F7FAFC;
|
||||
padding: 10px;
|
||||
|
||||
.wiz-choice:checked ~ .description {
|
||||
#itop-ticket-mgmt-simple-ticket-enhanced-portal:not(:checked),
|
||||
#itop-ticket-mgmt-itil-enhanced-portal:not(:checked) {
|
||||
~ .description::after {
|
||||
content: "Legacy portal is no longer part of iTop, by leaving this option unchecked your portal users won't be able to access iTop anymore.";
|
||||
display: block;
|
||||
margin-top: 0.5em;
|
||||
font-weight: bold;
|
||||
color: $legacy-portal-removal-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
body {
|
||||
font-size: 1.17rem;
|
||||
font-family: "Raleway";
|
||||
@@ -601,36 +595,6 @@ body {
|
||||
color: $ibo-color-blue-700;
|
||||
font-size: $ibo-font-size-200;
|
||||
}
|
||||
.setup-extension--missing .setup-extension--icon{
|
||||
color:#a00000;
|
||||
}
|
||||
.setup-extension-tag {
|
||||
display: inline-flex;
|
||||
background-color: grey;
|
||||
border-radius: 8px;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
margin-right: 3px;
|
||||
&.installed{
|
||||
background-color:#9eff9e
|
||||
}
|
||||
&.notinstalled{
|
||||
background-color:#ed9eff
|
||||
}
|
||||
&.tobeinstalled{
|
||||
background-color:#9ef0ff
|
||||
}
|
||||
&.tobeuninstalled{
|
||||
background-color:#ff9e9e
|
||||
}
|
||||
&.notuninstallable{
|
||||
background-color:#ffc98c
|
||||
}
|
||||
&.removed{
|
||||
background-color: #969594
|
||||
}
|
||||
}
|
||||
|
||||
.setup--wizard-choice--label + .setup--wizard-choice--more-info {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
@@ -682,6 +646,9 @@ body {
|
||||
overflow: auto;
|
||||
text-align: center;
|
||||
}
|
||||
#installation_progress {
|
||||
display: none;
|
||||
}
|
||||
#fresh_content{
|
||||
border: 0;
|
||||
min-height: 300px;
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace Combodo\iTop\DBTools\Service;
|
||||
use CMDBSource;
|
||||
use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
use IssueLog;
|
||||
|
||||
class DBToolsUtils
|
||||
{
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSetUIBlockFactory;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Service\Events\EventService;
|
||||
use Combodo\iTop\Service\Events\iEventServiceSetup;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
|
||||
|
||||
class AttachmentPlugIn implements iApplicationUIExtension, iEventServiceSetup
|
||||
{
|
||||
@@ -238,14 +236,10 @@ class AttachmentPlugIn implements iApplicationUIExtension, iEventServiceSetup
|
||||
}
|
||||
$oAttachmentsRenderer = AttachmentsRendererFactory::GetInstance($oPage, $sObjClass, $iObjKey, $sTransactionId);
|
||||
|
||||
$iCount = $oAttachmentsRenderer->GetAttachmentsSet()->Count() + $oAttachmentsRenderer->GetTempAttachmentsSet()->Count();
|
||||
$sTitle = ($iCount > 0) ? Dict::Format('Attachments:TabTitle_Count', $iCount) : Dict::S('Attachments:EmptyTabTitle');
|
||||
if ($this->GetAttachmentsPosition() === 'relations') {
|
||||
$iCount = $oAttachmentsRenderer->GetAttachmentsSet()->Count() + $oAttachmentsRenderer->GetTempAttachmentsSet()->Count();
|
||||
$sTitle = ($iCount > 0) ? Dict::Format('Attachments:TabTitle_Count', $iCount) : Dict::S('Attachments:EmptyTabTitle');
|
||||
$oPage->SetCurrentTab('Attachments:Tab', $sTitle);
|
||||
} else {
|
||||
$oBlock = FieldSetUIBlockFactory::MakeStandard($sTitle);
|
||||
$oBlock->AddSubBlock(new Html(''));
|
||||
$oPage->AddUiBlock($oBlock);
|
||||
}
|
||||
|
||||
$bIsReadOnlyState = self::IsReadonlyState($oObject, $oObject->GetState(), AttachmentPlugIn::ENUM_GUI_BACKOFFICE);
|
||||
|
||||
@@ -67,17 +67,15 @@ class EventListener implements iEventServiceSetup
|
||||
/** @var \DBObject $oAttachment */
|
||||
$oAttachment = $oEventData->Get('object');
|
||||
$oHostObj = MetaModel::GetObject($oAttachment->Get('item_class'), $oAttachment->Get('item_id'), false /* false to avoid exception during trigger */, true);
|
||||
if ($oHostObj != null) {
|
||||
/** @var \ormDocument $oDocument */
|
||||
$oDocument = $oEventData->Get('document');
|
||||
/** @var \ormDocument $oDocument */
|
||||
$oDocument = $oEventData->Get('document');
|
||||
|
||||
$this->OnAttachmentActivateTriggers(
|
||||
$oHostObj,
|
||||
$oAttachment,
|
||||
$oDocument,
|
||||
TriggerOnAttachmentDownload::class
|
||||
);
|
||||
}
|
||||
$this->OnAttachmentActivateTriggers(
|
||||
$oHostObj,
|
||||
$oAttachment,
|
||||
$oDocument,
|
||||
TriggerOnAttachmentDownload::class
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -323,38 +323,18 @@
|
||||
<value id="production">
|
||||
<code>production</code>
|
||||
<rank>30</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>20</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="stock">
|
||||
<code>stock</code>
|
||||
<rank>10</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-neutral-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-neutral-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="obsolete">
|
||||
<code>obsolete</code>
|
||||
<rank>40</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
</values>
|
||||
<sql>status</sql>
|
||||
@@ -6917,29 +6897,14 @@
|
||||
<value id="production">
|
||||
<code>production</code>
|
||||
<rank>20</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="obsolete">
|
||||
<code>obsolete</code>
|
||||
<rank>30</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
</values>
|
||||
<sql>status</sql>
|
||||
|
||||
@@ -92,7 +92,7 @@ final class CoreUpdater
|
||||
$sFinalEnv = 'production';
|
||||
$oRuntimeEnv = new RunTimeEnvironmentCoreUpdater($sFinalEnv, false);
|
||||
$oRuntimeEnv->CheckDirectories($sFinalEnv);
|
||||
$oRuntimeEnv->CompileFrom($sFinalEnv);
|
||||
$oRuntimeEnv->CompileFrom('production');
|
||||
|
||||
$oRuntimeEnv->Rollback();
|
||||
|
||||
@@ -155,13 +155,21 @@ final class CoreUpdater
|
||||
APPROOT.'extensions',
|
||||
];
|
||||
$aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $aDirsToScanForModules);
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation');
|
||||
$aSelectedModules = [];
|
||||
foreach ($aAvailableModules as $sModuleId => $aModule) {
|
||||
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
|
||||
continue;
|
||||
} else {
|
||||
$aSelectedModules[] = $sModuleId;
|
||||
}
|
||||
}
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'BeforeDatabaseCreation');
|
||||
$oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade');
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation');
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation');
|
||||
$oRuntimeEnv->UpdatePredefinedObjects();
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup');
|
||||
$oRuntimeEnv->LoadData($aAvailableModules, false /* no sample data*/);
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad');
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup');
|
||||
$oRuntimeEnv->LoadData($aAvailableModules, $aSelectedModules, false /* no sample data*/);
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDataLoad');
|
||||
$sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion();
|
||||
$oExtensionsMap = new iTopExtensionsMap();
|
||||
// Default choices = as before
|
||||
@@ -179,7 +187,7 @@ final class CoreUpdater
|
||||
$oRuntimeEnv->RecordInstallation(
|
||||
$oConfig,
|
||||
$sDataModelVersion,
|
||||
array_keys($aAvailableModules),
|
||||
$aSelectedModules,
|
||||
$aSelectedExtensionCodes,
|
||||
'Done by the iTop Core Updater'
|
||||
);
|
||||
|
||||
@@ -135,7 +135,7 @@ class RunTimeEnvironmentCoreUpdater extends RunTimeEnvironment
|
||||
$aAvailableModules[$oModule->GetName()] = $oModule;
|
||||
}
|
||||
// TODO check the auto-selected modules here
|
||||
foreach ($this->GetExtensionMap()->GetAllExtensions() as $oExtension) {
|
||||
foreach ($this->oExtensionsMap->GetAllExtensions() as $oExtension) {
|
||||
if ($oExtension->bMarkedAsChosen) {
|
||||
foreach ($oExtension->aModules as $sModuleName) {
|
||||
if (!isset($aRet[$sModuleName]) && isset($aAvailableModules[$sModuleName])) {
|
||||
|
||||
@@ -24,13 +24,129 @@
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
use Combodo\iTop\HubConnector\Controller\HubController;
|
||||
use Combodo\iTop\Application\WebPage\JsonPage;
|
||||
|
||||
require_once(APPROOT.'application/utils.inc.php');
|
||||
require_once(APPROOT.'core/log.class.inc.php');
|
||||
IssueLog::Enable(APPROOT.'log/error.log');
|
||||
|
||||
require_once(__DIR__.'/src/Controller/HubController.php');
|
||||
require_once(APPROOT.'setup/runtimeenv.class.inc.php');
|
||||
require_once(APPROOT.'setup/backup.class.inc.php');
|
||||
require_once(APPROOT.'core/mutex.class.inc.php');
|
||||
require_once(APPROOT.'core/dict.class.inc.php');
|
||||
require_once(APPROOT.'setup/xmldataloader.class.inc.php');
|
||||
require_once(__DIR__.'/hubruntimeenvironment.class.inc.php');
|
||||
|
||||
/**
|
||||
* Overload of DBBackup to handle logging
|
||||
*/
|
||||
class DBBackupWithErrorReporting extends DBBackup
|
||||
{
|
||||
protected $aInfos = [];
|
||||
|
||||
protected $aErrors = [];
|
||||
|
||||
protected function LogInfo($sMsg)
|
||||
{
|
||||
$aInfos[] = $sMsg;
|
||||
}
|
||||
|
||||
protected function LogError($sMsg)
|
||||
{
|
||||
IssueLog::Error($sMsg);
|
||||
$aErrors[] = $sMsg;
|
||||
}
|
||||
|
||||
public function GetInfos()
|
||||
{
|
||||
return $this->aInfos;
|
||||
}
|
||||
|
||||
public function GetErrors()
|
||||
{
|
||||
return $this->aErrors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $sTargetFile
|
||||
* @throws Exception
|
||||
* @return DBBackupWithErrorReporting
|
||||
*/
|
||||
function DoBackup($sTargetFile)
|
||||
{
|
||||
// Make sure the target directory exists
|
||||
$sBackupDir = dirname($sTargetFile);
|
||||
SetupUtils::builddir($sBackupDir);
|
||||
|
||||
$oBackup = new DBBackupWithErrorReporting();
|
||||
$oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', ''));
|
||||
$sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
|
||||
|
||||
$oMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment());
|
||||
$oMutex->Lock();
|
||||
try {
|
||||
$oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile);
|
||||
} catch (Exception $e) {
|
||||
$oMutex->Unlock();
|
||||
throw $e;
|
||||
}
|
||||
$oMutex->Unlock();
|
||||
return $oBackup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the status of the current ajax execution (as a JSON structure)
|
||||
*
|
||||
* @param string $sMessage
|
||||
* @param bool $bSuccess
|
||||
* @param number $iErrorCode
|
||||
* @param array $aMoreFields
|
||||
* Extra fields to pass to the caller, if needed
|
||||
*/
|
||||
function ReportStatus($sMessage, $bSuccess, $iErrorCode = 0, $aMoreFields = [])
|
||||
{
|
||||
// Do not use AjaxPage during setup phases, because it uses InterfaceDiscovery in Twig compilation
|
||||
$oPage = new JsonPage();
|
||||
$aResult = [
|
||||
'code' => $iErrorCode,
|
||||
'message' => $sMessage,
|
||||
'fields' => $aMoreFields,
|
||||
];
|
||||
$oPage->SetData($aResult);
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
$oPage->output();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to output the status of a successful execution
|
||||
*
|
||||
* @param string $sMessage
|
||||
* @param array $aMoreFields
|
||||
* Extra fields to pass to the caller, if needed
|
||||
*/
|
||||
function ReportSuccess($sMessage, $aMoreFields = [])
|
||||
{
|
||||
ReportStatus($sMessage, true, 0, $aMoreFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to output the status of a failed execution
|
||||
*
|
||||
* @param string $sMessage
|
||||
* @param number $iErrorCode
|
||||
* @param array $aMoreFields
|
||||
* Extra fields to pass to the caller, if needed
|
||||
*/
|
||||
function ReportError($sMessage, $iErrorCode, $aMoreFields = [])
|
||||
{
|
||||
if ($iErrorCode == 0) {
|
||||
// 0 means no error, so change it if no meaningful error code is supplied
|
||||
$iErrorCode = -1;
|
||||
}
|
||||
ReportStatus($sMessage, false, $iErrorCode, $aMoreFields);
|
||||
}
|
||||
|
||||
try {
|
||||
SetupUtils::ExitMaintenanceMode(false); // Reset maintenance mode in case of problem
|
||||
@@ -67,7 +183,7 @@ try {
|
||||
foreach ($aChecks as $oCheckResult) {
|
||||
if ($oCheckResult->iSeverity == CheckResult::ERROR) {
|
||||
$bFailed = true;
|
||||
HubController::GetInstance()->ReportError($oCheckResult->sLabel, -2);
|
||||
ReportError($oCheckResult->sLabel, -2);
|
||||
}
|
||||
}
|
||||
if (!$bFailed) {
|
||||
@@ -75,27 +191,169 @@ try {
|
||||
$fFreeSpace = SetupUtils::CheckDiskSpace($sDBBackupPath);
|
||||
if ($fFreeSpace !== false) {
|
||||
$sMessage = Dict::Format('iTopHub:BackupFreeDiskSpaceIn', SetupUtils::HumanReadableSize($fFreeSpace), dirname($sDBBackupPath));
|
||||
HubController::GetInstance()->ReportSuccess($sMessage);
|
||||
ReportSuccess($sMessage);
|
||||
} else {
|
||||
HubController::GetInstance()->ReportError(Dict::S('iTopHub:FailedToCheckFreeDiskSpace'), -1);
|
||||
ReportError(Dict::S('iTopHub:FailedToCheckFreeDiskSpace'), -1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'do_backup':
|
||||
HubController::GetInstance()->LaunchBackup();
|
||||
require_once(APPROOT.'/application/startup.inc.php');
|
||||
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
|
||||
LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
|
||||
|
||||
try {
|
||||
if (MetaModel::GetConfig()->Get('demo_mode')) {
|
||||
throw new Exception('Sorry the installation of extensions is not allowed in demo mode');
|
||||
}
|
||||
SetupLog::Info('Backup starts...');
|
||||
set_time_limit(0);
|
||||
$sBackupPath = APPROOT.'/data/backups/manual/backup-';
|
||||
$iSuffix = 1;
|
||||
$sSuffix = '';
|
||||
// Generate a unique name...
|
||||
do {
|
||||
$sBackupFile = $sBackupPath.date('Y-m-d-His').$sSuffix;
|
||||
$sSuffix = '-'.$iSuffix;
|
||||
$iSuffix++ ;
|
||||
} while (file_exists($sBackupFile));
|
||||
|
||||
$oBackup = DoBackup($sBackupFile);
|
||||
$aErrors = $oBackup->GetErrors();
|
||||
if (count($aErrors) > 0) {
|
||||
SetupLog::Error('Backup failed.');
|
||||
SetupLog::Error(implode("\n", $aErrors));
|
||||
ReportError(Dict::S('iTopHub:BackupFailed'), -1, $aErrors);
|
||||
} else {
|
||||
SetupLog::Info('Backup successfully completed.');
|
||||
ReportSuccess(Dict::S('iTopHub:BackupOk'));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
SetupLog::Error($e->getMessage());
|
||||
ReportError($e->getMessage(), $e->getCode());
|
||||
}
|
||||
break;
|
||||
|
||||
case 'compile':
|
||||
HubController::GetInstance()->LaunchCompile();
|
||||
SetupLog::Info('Deployment starts...');
|
||||
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
|
||||
if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) {
|
||||
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
|
||||
}
|
||||
// First step: prepare the datamodel, if it fails, roll-back
|
||||
$aSelectedExtensionCodes = utils::ReadParam('extension_codes', []);
|
||||
$aSelectedExtensionDirs = utils::ReadParam('extension_dirs', []);
|
||||
|
||||
$oRuntimeEnv = new HubRunTimeEnvironment('production', false); // use a temp environment: production-build
|
||||
$oRuntimeEnv->MoveSelectedExtensions(APPROOT.'/data/downloaded-extensions/', $aSelectedExtensionDirs);
|
||||
|
||||
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
|
||||
if ($oConfig->Get('demo_mode')) {
|
||||
throw new Exception('Sorry the installation of extensions is not allowed in demo mode');
|
||||
}
|
||||
|
||||
$aSelectModules = $oRuntimeEnv->CompileFrom('production', false); // WARNING symlinks does not seem to be compatible with manual Commit
|
||||
|
||||
$oRuntimeEnv->UpdateIncludes($oConfig);
|
||||
|
||||
$oRuntimeEnv->InitDataModel($oConfig, true /* model only */);
|
||||
|
||||
// Safety check: check the inter dependencies, will throw an exception in case of inconsistency
|
||||
$oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true);
|
||||
|
||||
$oRuntimeEnv->CheckMetaModel(); // Will throw an exception if a problem is detected
|
||||
|
||||
// Everything seems Ok so far, commit in env-production!
|
||||
$oRuntimeEnv->WriteConfigFileSafe($oConfig);
|
||||
$oRuntimeEnv->Commit();
|
||||
|
||||
// Report the success in a way that will be detected by the ajax caller
|
||||
SetupLog::Info('Compilation completed...');
|
||||
ReportSuccess('Ok'); // No access to Dict::S here
|
||||
break;
|
||||
|
||||
case 'move_to_production':
|
||||
HubController::GetInstance()->LaunchDeploy();
|
||||
// Second step: update the schema and the data
|
||||
// Everything happening below is based on env-production
|
||||
$oRuntimeEnv = new RunTimeEnvironment('production', true);
|
||||
|
||||
try {
|
||||
SetupLog::Info('Move to production starts...');
|
||||
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
|
||||
if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) {
|
||||
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
|
||||
}
|
||||
unlink(utils::GetDataPath().'hub/compile_authent');
|
||||
// Load the "production" config file to clone & update it
|
||||
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
|
||||
SetupUtils::EnterReadOnlyMode($oConfig);
|
||||
|
||||
$oRuntimeEnv->InitDataModel($oConfig, true /* model only */);
|
||||
|
||||
$aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true);
|
||||
|
||||
$aSelectedModules = [];
|
||||
foreach ($aAvailableModules as $sModuleId => $aModule) {
|
||||
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
|
||||
continue;
|
||||
} else {
|
||||
$aSelectedModules[] = $sModuleId;
|
||||
}
|
||||
}
|
||||
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'BeforeDatabaseCreation');
|
||||
|
||||
$oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade');
|
||||
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation');
|
||||
|
||||
$oRuntimeEnv->UpdatePredefinedObjects();
|
||||
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup');
|
||||
|
||||
$oRuntimeEnv->LoadData($aAvailableModules, $aSelectedModules, false /* no sample data*/);
|
||||
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDataLoad');
|
||||
|
||||
// Record the installation so that the "about box" knows about the installed modules
|
||||
$sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion();
|
||||
|
||||
$oExtensionsMap = new iTopExtensionsMap();
|
||||
|
||||
// Default choices = as before
|
||||
$oExtensionsMap->LoadChoicesFromDatabase($oConfig);
|
||||
foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) {
|
||||
// Plus all "remote" extensions
|
||||
if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) {
|
||||
$oExtensionsMap->MarkAsChosen($oExtension->sCode);
|
||||
}
|
||||
}
|
||||
$aSelectedExtensionCodes = [];
|
||||
foreach ($oExtensionsMap->GetChoices() as $oExtension) {
|
||||
$aSelectedExtensionCodes[] = $oExtension->sCode;
|
||||
}
|
||||
$aSelectedExtensions = $oExtensionsMap->GetChoices();
|
||||
$oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, $aSelectedModules, $aSelectedExtensionCodes, 'Done by the iTop Hub Connector');
|
||||
|
||||
// Report the success in a way that will be detected by the ajax caller
|
||||
SetupLog::Info('Deployment successfully completed.');
|
||||
ReportSuccess(Dict::S('iTopHub:CompiledOK'));
|
||||
} catch (Exception $e) {
|
||||
if (file_exists(utils::GetDataPath().'hub/compile_authent')) {
|
||||
unlink(utils::GetDataPath().'hub/compile_authent');
|
||||
}
|
||||
// Note: at this point, the dictionnary is not necessarily loaded
|
||||
SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage());
|
||||
SetupLog::Error('Debug trace: '.$e->getTraceAsString());
|
||||
ReportError($e->getMessage(), $e->getCode());
|
||||
} finally {
|
||||
SetupUtils::ExitReadOnlyMode();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
HubController::GetInstance()->ReportError("Invalid operation: '$sOperation'", -1);
|
||||
ReportError("Invalid operation: '$sOperation'", -1);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage());
|
||||
@@ -103,5 +361,5 @@ try {
|
||||
|
||||
utils::PopArchiveMode();
|
||||
|
||||
HubController::GetInstance()->ReportError($e->getMessage(), $e->getCode());
|
||||
ReportError($e->getMessage(), $e->getCode());
|
||||
}
|
||||
|
||||
@@ -1,83 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\HubConnector\setup;
|
||||
|
||||
use Config;
|
||||
use Exception;
|
||||
use RunTimeEnvironment;
|
||||
use SetupUtils;
|
||||
|
||||
class HubRunTimeEnvironment extends RunTimeEnvironment
|
||||
{
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $sEnvironment
|
||||
* @param string $bAutoCommit
|
||||
*/
|
||||
public function __construct($sEnvironment = 'production', $bAutoCommit = true)
|
||||
{
|
||||
parent::__construct($sEnvironment, $bAutoCommit);
|
||||
|
||||
if ($sEnvironment != $this->sTargetEnv) {
|
||||
if (is_dir(APPROOT.'/env-'.$this->sTargetEnv)) {
|
||||
SetupUtils::rrmdir(APPROOT.'/env-'.$this->sTargetEnv);
|
||||
}
|
||||
if (is_dir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) {
|
||||
SetupUtils::rrmdir(APPROOT.'/data/'.$this->sTargetEnv.'-modules');
|
||||
}
|
||||
|
||||
if ($sEnvironment != $this->sTargetEnv)
|
||||
{
|
||||
if (is_dir(APPROOT.'/env-'.$this->sTargetEnv))
|
||||
{
|
||||
SetupUtils::rrmdir(APPROOT.'/env-'.$this->sTargetEnv);
|
||||
}
|
||||
if (is_dir(APPROOT.'/data/'.$this->sTargetEnv.'-modules'))
|
||||
{
|
||||
SetupUtils::rrmdir(APPROOT.'/data/'.$this->sTargetEnv.'-modules');
|
||||
}
|
||||
SetupUtils::copydir(APPROOT.'/data/'.$sEnvironment.'-modules', APPROOT.'/data/'.$this->sTargetEnv.'-modules');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the includes for the target environment
|
||||
*
|
||||
* @param Config $oConfig
|
||||
*/
|
||||
public function UpdateIncludes(Config $oConfig)
|
||||
{
|
||||
$oConfig->UpdateIncludes('env-'.$this->sTargetEnv); // TargetEnv != FinalEnv
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Move an extension (path to folder of this extension) to the target environment
|
||||
*
|
||||
* @param string $sExtensionDirectory The folder of the extension
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function MoveExtension($sExtensionDirectory)
|
||||
{
|
||||
if (!is_dir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) {
|
||||
if (!mkdir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) {
|
||||
throw new Exception("ERROR: failed to create directory:'".(APPROOT.'/data/'.$this->sTargetEnv.'-modules')."'");
|
||||
}
|
||||
if (!is_dir(APPROOT.'/data/'.$this->sTargetEnv.'-modules'))
|
||||
{
|
||||
if (!mkdir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) throw new Exception("ERROR: failed to create directory:'".(APPROOT.'/data/'.$this->sTargetEnv.'-modules')."'");
|
||||
}
|
||||
$sDestinationPath = APPROOT.'/data/'.$this->sTargetEnv.'-modules/';
|
||||
|
||||
|
||||
// Make sure that the destination directory of the extension does not already exist
|
||||
if (is_dir($sDestinationPath.basename($sExtensionDirectory))) {
|
||||
// Cleanup before moving...
|
||||
SetupUtils::rrmdir($sDestinationPath.basename($sExtensionDirectory));
|
||||
}
|
||||
if (!rename($sExtensionDirectory, $sDestinationPath.basename($sExtensionDirectory))) {
|
||||
throw new Exception("ERROR: failed move directory:'$sExtensionDirectory' to '".$sDestinationPath.basename($sExtensionDirectory)."'");
|
||||
if (is_dir($sDestinationPath.basename($sExtensionDirectory)))
|
||||
{
|
||||
// Cleanup before moving...
|
||||
SetupUtils::rrmdir($sDestinationPath.basename($sExtensionDirectory));
|
||||
}
|
||||
if (!rename($sExtensionDirectory, $sDestinationPath.basename($sExtensionDirectory))) throw new Exception("ERROR: failed move directory:'$sExtensionDirectory' to '".$sDestinationPath.basename($sExtensionDirectory)."'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Move the selected extensions located in the given directory in data/<target-env>-modules
|
||||
*
|
||||
* @param string $sDownloadedExtensionsDir The directory to scan
|
||||
* @param string[] $aSelectedExtensionDirs The list of folders to move
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function MoveSelectedExtensions($sDownloadedExtensionsDir, $aSelectedExtensionDirs)
|
||||
{
|
||||
foreach (glob($sDownloadedExtensionsDir.'*', GLOB_ONLYDIR) as $sExtensionDir) {
|
||||
if (in_array(basename($sExtensionDir), $aSelectedExtensionDirs)) {
|
||||
foreach(glob($sDownloadedExtensionsDir.'*', GLOB_ONLYDIR) as $sExtensionDir)
|
||||
{
|
||||
if (in_array(basename($sExtensionDir), $aSelectedExtensionDirs))
|
||||
{
|
||||
$this->MoveExtension($sExtensionDir);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ function DisplayStatus(WebPage $oPage)
|
||||
if (is_dir($sPath)) {
|
||||
$aExtraDirs[] = $sPath; // Also read the extra downloaded-modules directory
|
||||
}
|
||||
$oExtensionsMap = new iTopExtensionsMap('production', $aExtraDirs);
|
||||
$oExtensionsMap = new iTopExtensionsMap('production', true, $aExtraDirs);
|
||||
$oExtensionsMap->LoadChoicesFromDatabase(MetaModel::GetConfig());
|
||||
|
||||
foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) {
|
||||
@@ -154,7 +154,7 @@ function DoInstall(WebPage $oPage)
|
||||
if (is_dir($sPath)) {
|
||||
$aExtraDirs[] = $sPath; // Also read the extra downloaded-modules directory
|
||||
}
|
||||
$oExtensionsMap = new iTopExtensionsMap('production', $aExtraDirs);
|
||||
$oExtensionsMap = new iTopExtensionsMap('production', true, $aExtraDirs);
|
||||
$oExtensionsMap->LoadChoicesFromDatabase(MetaModel::GetConfig());
|
||||
|
||||
foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) {
|
||||
|
||||
@@ -186,7 +186,9 @@ function collect_configuration()
|
||||
|
||||
// iTop modules
|
||||
$oConfig = MetaModel::GetConfig();
|
||||
$aInstalledModules = ModuleInstallationRepository::GetInstance()->ReadFromDB($oConfig);
|
||||
$sLatestInstallationDate = CMDBSource::QueryToScalar("SELECT max(installed) FROM ".$oConfig->Get('db_subname')."priv_module_install");
|
||||
// Get the latest installed modules, without the "root" ones (iTop version and datamodel version)
|
||||
$aInstalledModules = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install WHERE installed = '".$sLatestInstallationDate."' AND parent_id != 0");
|
||||
|
||||
foreach ($aInstalledModules as $aDBInfo) {
|
||||
$aConfiguration['itop_modules'][$aDBInfo['name']] = $aDBInfo['version'];
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\HubConnector\Controller;
|
||||
|
||||
use Combodo\iTop\Application\WebPage\JsonPage;
|
||||
use Combodo\iTop\HubConnector\Model\DBBackupWithErrorReporting;
|
||||
use Combodo\iTop\HubConnector\setup\HubRunTimeEnvironment;
|
||||
use Config;
|
||||
use Dict;
|
||||
use Exception;
|
||||
use iTopExtension;
|
||||
use iTopExtensionsMap;
|
||||
use iTopMutex;
|
||||
use LoginWebPage;
|
||||
use MetaModel;
|
||||
use MFCompiler;
|
||||
use RunTimeEnvironment;
|
||||
use SecurityException;
|
||||
use SetupLog;
|
||||
use SetupUtils;
|
||||
use utils;
|
||||
|
||||
require_once(APPROOT.'setup/runtimeenv.class.inc.php');
|
||||
require_once(APPROOT.'setup/backup.class.inc.php');
|
||||
require_once(APPROOT.'core/mutex.class.inc.php');
|
||||
require_once(APPROOT.'core/dict.class.inc.php');
|
||||
require_once(APPROOT.'setup/xmldataloader.class.inc.php');
|
||||
require_once(__DIR__.'/../setup/hubruntimeenvironment.class.inc.php');
|
||||
|
||||
class HubController
|
||||
{
|
||||
private static HubController $oInstance;
|
||||
protected $bOutputHeaders = false;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
final public static function GetInstance(): HubController
|
||||
{
|
||||
if (!isset(self::$oInstance)) {
|
||||
self::$oInstance = new HubController();
|
||||
}
|
||||
|
||||
return self::$oInstance;
|
||||
}
|
||||
|
||||
final public static function SetInstance(?HubController $oInstance): void
|
||||
{
|
||||
self::$oInstance = $oInstance;
|
||||
}
|
||||
|
||||
public function LaunchBackup()
|
||||
{
|
||||
require_once(APPROOT.'/application/startup.inc.php');
|
||||
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
|
||||
LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
|
||||
|
||||
try {
|
||||
if (MetaModel::GetConfig()->Get('demo_mode')) {
|
||||
throw new Exception('Sorry the installation of extensions is not allowed in demo mode');
|
||||
}
|
||||
SetupLog::Info('Backup starts...');
|
||||
set_time_limit(0);
|
||||
$sBackupPath = APPROOT.'/data/backups/manual/backup-';
|
||||
$iSuffix = 1;
|
||||
$sSuffix = '';
|
||||
// Generate a unique name...
|
||||
do {
|
||||
$sBackupFile = $sBackupPath.date('Y-m-d-His').$sSuffix;
|
||||
$sSuffix = '-'.$iSuffix;
|
||||
$iSuffix++ ;
|
||||
} while (file_exists($sBackupFile));
|
||||
|
||||
$oBackup = $this->DoBackup($sBackupFile);
|
||||
$aErrors = $oBackup->GetErrors();
|
||||
if (count($aErrors) > 0) {
|
||||
SetupLog::Error('Backup failed.');
|
||||
SetupLog::Error(implode("\n", $aErrors));
|
||||
$this->ReportError(Dict::S('iTopHub:BackupFailed'), -1, $aErrors);
|
||||
} else {
|
||||
SetupLog::Info('Backup successfully completed.');
|
||||
$this->ReportSuccess(Dict::S('iTopHub:BackupOk'));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
SetupLog::Error($e->getMessage());
|
||||
$this->ReportError($e->getMessage(), $e->getCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $sTargetFile
|
||||
* @throws Exception
|
||||
* @return DBBackupWithErrorReporting
|
||||
*/
|
||||
public function DoBackup($sTargetFile): DBBackupWithErrorReporting
|
||||
{
|
||||
// Make sure the target directory exists
|
||||
$sBackupDir = dirname($sTargetFile);
|
||||
SetupUtils::builddir($sBackupDir);
|
||||
|
||||
$oBackup = new DBBackupWithErrorReporting();
|
||||
$oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', ''));
|
||||
$sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
|
||||
|
||||
$oMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment());
|
||||
$oMutex->Lock();
|
||||
try {
|
||||
$oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile);
|
||||
} catch (Exception $e) {
|
||||
$oMutex->Unlock();
|
||||
throw $e;
|
||||
}
|
||||
$oMutex->Unlock();
|
||||
return $oBackup;
|
||||
}
|
||||
|
||||
public function LaunchCompile()
|
||||
{
|
||||
SetupLog::Info('Deployment starts...');
|
||||
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
|
||||
if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) {
|
||||
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
|
||||
}
|
||||
// First step: prepare the datamodel, if it fails, roll-back
|
||||
$aSelectedExtensionCodes = utils::ReadParam('extension_codes', []);
|
||||
$aSelectedExtensionDirs = utils::ReadParam('extension_dirs', []);
|
||||
|
||||
$oRuntimeEnv = new HubRunTimeEnvironment('production', false); // use a temp environment: production-build
|
||||
$oRuntimeEnv->MoveSelectedExtensions(APPROOT.'/data/downloaded-extensions/', $aSelectedExtensionDirs);
|
||||
|
||||
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
|
||||
if ($oConfig->Get('demo_mode')) {
|
||||
throw new Exception('Sorry the installation of extensions is not allowed in demo mode');
|
||||
}
|
||||
|
||||
$aSelectModules = $oRuntimeEnv->CompileFrom('production'); // WARNING symlinks does not seem to be compatible with manual Commit
|
||||
|
||||
$oRuntimeEnv->UpdateIncludes($oConfig);
|
||||
|
||||
$oRuntimeEnv->InitDataModel($oConfig, true /* model only */);
|
||||
|
||||
// Safety check: check the inter dependencies, will throw an exception in case of inconsistency
|
||||
$oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true);
|
||||
|
||||
$oRuntimeEnv->CheckMetaModel(); // Will throw an exception if a problem is detected
|
||||
|
||||
// Everything seems Ok so far, commit in env-production!
|
||||
$oRuntimeEnv->WriteConfigFileSafe($oConfig);
|
||||
$oRuntimeEnv->Commit();
|
||||
|
||||
// Report the success in a way that will be detected by the ajax caller
|
||||
SetupLog::Info('Compilation completed...');
|
||||
|
||||
$this->ReportSuccess('Ok'); // No access to Dict::S here
|
||||
}
|
||||
|
||||
public function LaunchDeploy()
|
||||
{
|
||||
// Second step: update the schema and the data
|
||||
// Everything happening below is based on env-production
|
||||
$oRuntimeEnv = new RunTimeEnvironment('production', true);
|
||||
|
||||
try {
|
||||
SetupLog::Info('Move to production starts...');
|
||||
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
|
||||
if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) {
|
||||
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
|
||||
}
|
||||
unlink(utils::GetDataPath().'hub/compile_authent');
|
||||
// Load the "production" config file to clone & update it
|
||||
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
|
||||
SetupUtils::EnterReadOnlyMode($oConfig);
|
||||
|
||||
$oRuntimeEnv->InitDataModel($oConfig, true /* model only */);
|
||||
|
||||
$aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true);
|
||||
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation');
|
||||
|
||||
$oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade');
|
||||
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation');
|
||||
|
||||
$oRuntimeEnv->UpdatePredefinedObjects();
|
||||
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup');
|
||||
|
||||
$oRuntimeEnv->LoadData($aAvailableModules, false /* no sample data*/);
|
||||
|
||||
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad');
|
||||
|
||||
// Record the installation so that the "about box" knows about the installed modules
|
||||
$sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion();
|
||||
|
||||
$oExtensionsMap = new iTopExtensionsMap();
|
||||
|
||||
// Default choices = as before
|
||||
$oExtensionsMap->LoadChoicesFromDatabase($oConfig);
|
||||
foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) {
|
||||
// Plus all "remote" extensions
|
||||
if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) {
|
||||
$oExtensionsMap->MarkAsChosen($oExtension->sCode);
|
||||
}
|
||||
}
|
||||
$aSelectedExtensionCodes = [];
|
||||
foreach ($oExtensionsMap->GetChoices() as $oExtension) {
|
||||
$aSelectedExtensionCodes[] = $oExtension->sCode;
|
||||
}
|
||||
$aSelectedExtensions = $oExtensionsMap->GetChoices();
|
||||
$oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, array_keys($aAvailableModules), $aSelectedExtensionCodes, 'Done by the iTop Hub Connector');
|
||||
|
||||
// Report the success in a way that will be detected by the ajax caller
|
||||
SetupLog::Info('Deployment successfully completed.');
|
||||
$this->ReportSuccess(Dict::S('iTopHub:CompiledOK'));
|
||||
} catch (Exception $e) {
|
||||
if (file_exists(utils::GetDataPath().'hub/compile_authent')) {
|
||||
unlink(utils::GetDataPath().'hub/compile_authent');
|
||||
}
|
||||
// Note: at this point, the dictionnary is not necessarily loaded
|
||||
SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage());
|
||||
SetupLog::Error('Debug trace: '.$e->getTraceAsString());
|
||||
$this->ReportError($e->getMessage(), $e->getCode());
|
||||
} finally {
|
||||
SetupUtils::ExitReadOnlyMode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the status of the current ajax execution (as a JSON structure)
|
||||
*
|
||||
* @param string $sMessage
|
||||
* @param bool $bSuccess
|
||||
* @param number $iErrorCode
|
||||
* @param array $aMoreFields
|
||||
* Extra fields to pass to the caller, if needed
|
||||
*/
|
||||
public function ReportStatus($sMessage, $bSuccess, $iErrorCode = 0, $aMoreFields = [])
|
||||
{
|
||||
// Do not use AjaxPage during setup phases, because it uses InterfaceDiscovery in Twig compilation
|
||||
$this->oLastJsonPage = new JsonPage();
|
||||
$this->oLastJsonPage->SetOutputHeaders($this->bOutputHeaders);
|
||||
$aResult = [
|
||||
'code' => $iErrorCode,
|
||||
'message' => $sMessage,
|
||||
'fields' => $aMoreFields,
|
||||
];
|
||||
$this->oLastJsonPage->SetData($aResult);
|
||||
$this->oLastJsonPage->SetOutputDataOnly(true);
|
||||
$this->oLastJsonPage->output();
|
||||
}
|
||||
|
||||
private ?JsonPage $oLastJsonPage = null;
|
||||
|
||||
public function GetLastJsonPage(): ?JsonPage
|
||||
{
|
||||
return $this->oLastJsonPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to output the status of a successful execution
|
||||
*
|
||||
* @param string $sMessage
|
||||
* @param array $aMoreFields
|
||||
* Extra fields to pass to the caller, if needed
|
||||
*/
|
||||
public function ReportSuccess($sMessage, $aMoreFields = [])
|
||||
{
|
||||
$this->ReportStatus($sMessage, true, 0, $aMoreFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to output the status of a failed execution
|
||||
*
|
||||
* @param string $sMessage
|
||||
* @param number $iErrorCode
|
||||
* @param array $aMoreFields
|
||||
* Extra fields to pass to the caller, if needed
|
||||
*/
|
||||
public function ReportError($sMessage, $iErrorCode, $aMoreFields = [])
|
||||
{
|
||||
if ($iErrorCode == 0) {
|
||||
// 0 means no error, so change it if no meaningful error code is supplied
|
||||
$iErrorCode = -1;
|
||||
}
|
||||
$this->ReportStatus($sMessage, false, $iErrorCode, $aMoreFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dont print headers for testing purpose mainly
|
||||
* @param bool bOutputHeaders
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function SetOutputHeaders(bool $bOutputHeaders): void
|
||||
{
|
||||
$this->bOutputHeaders = $bOutputHeaders;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\HubConnector\Model;
|
||||
|
||||
use DBBackup;
|
||||
use IssueLog;
|
||||
|
||||
/**
|
||||
* Overload of DBBackup to handle logging
|
||||
*/
|
||||
class DBBackupWithErrorReporting extends DBBackup
|
||||
{
|
||||
protected $aInfos = [];
|
||||
|
||||
protected $aErrors = [];
|
||||
|
||||
protected function LogInfo($sMsg)
|
||||
{
|
||||
$this->aInfos[] = $sMsg;
|
||||
}
|
||||
|
||||
protected function LogError($sMsg)
|
||||
{
|
||||
IssueLog::Error($sMsg);
|
||||
$this->aErrors[] = $sMsg;
|
||||
}
|
||||
|
||||
public function GetInfos(): array
|
||||
{
|
||||
return $this->aInfos;
|
||||
}
|
||||
|
||||
public function GetErrors(): array
|
||||
{
|
||||
return $this->aErrors;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ SetupWebPage::AddModule(
|
||||
//
|
||||
'dependencies' => [
|
||||
'itop-welcome-itil/3.1.0,',
|
||||
'itop-profiles-itil/3.1.0', //SuperUser id 117
|
||||
],
|
||||
'mandatory' => false,
|
||||
'visible' => true,
|
||||
|
||||
@@ -28,7 +28,6 @@ SetupWebPage::AddModule(
|
||||
'category' => 'Portal',
|
||||
// Setup
|
||||
'dependencies' => [
|
||||
'itop-attachments/3.2.1', //CMDBChangeOpAttachmentRemoved
|
||||
],
|
||||
'mandatory' => true,
|
||||
'visible' => false,
|
||||
|
||||
@@ -76,9 +76,6 @@
|
||||
<attribute id="finalclass"/>
|
||||
</attributes>
|
||||
</reconciliation>
|
||||
<obsolescence>
|
||||
<condition><![CDATA[status='obsolete']]></condition>
|
||||
</obsolescence>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="name" xsi:type="AttributeString">
|
||||
@@ -179,32 +176,17 @@
|
||||
<field id="status" xsi:type="AttributeEnum">
|
||||
<sort_type>rank</sort_type>
|
||||
<values>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="production">
|
||||
<code>production</code>
|
||||
<rank>20</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
</value>
|
||||
<value id="obsolete">
|
||||
<code>obsolete</code>
|
||||
<rank>30</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
</values>
|
||||
<sql>status</sql>
|
||||
@@ -1151,9 +1133,6 @@ public function PrefillSearchForm(&$aContextParam)
|
||||
<attribute id="organization_name"/>
|
||||
</attributes>
|
||||
</reconciliation>
|
||||
<obsolescence>
|
||||
<condition><![CDATA[status='obsolete']]></condition>
|
||||
</obsolescence>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="name" xsi:type="AttributeString">
|
||||
@@ -1205,32 +1184,17 @@ public function PrefillSearchForm(&$aContextParam)
|
||||
<field id="status" xsi:type="AttributeEnum">
|
||||
<sort_type>rank</sort_type>
|
||||
<values>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="production">
|
||||
<code>production</code>
|
||||
<rank>20</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
</value>
|
||||
<value id="obsolete">
|
||||
<code>obsolete</code>
|
||||
<rank>30</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
</values>
|
||||
<sql>status</sql>
|
||||
@@ -1573,9 +1537,6 @@ public function PrefillSearchForm(&$aContextParam)
|
||||
<attribute id="service_name"/>
|
||||
</attributes>
|
||||
</reconciliation>
|
||||
<obsolescence>
|
||||
<condition><![CDATA[status='obsolete']]></condition>
|
||||
</obsolescence>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="name" xsi:type="AttributeString">
|
||||
@@ -1624,32 +1585,17 @@ public function PrefillSearchForm(&$aContextParam)
|
||||
<field id="status" xsi:type="AttributeEnum">
|
||||
<sort_type>rank</sort_type>
|
||||
<values>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="production">
|
||||
<code>production</code>
|
||||
<rank>20</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
</value>
|
||||
<value id="obsolete">
|
||||
<code>obsolete</code>
|
||||
<rank>30</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
</values>
|
||||
<sql>status</sql>
|
||||
|
||||
@@ -76,9 +76,6 @@
|
||||
<attribute id="finalclass"/>
|
||||
</attributes>
|
||||
</reconciliation>
|
||||
<obsolescence>
|
||||
<condition><![CDATA[status='obsolete']]></condition>
|
||||
</obsolescence>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="name" xsi:type="AttributeString">
|
||||
@@ -179,32 +176,17 @@
|
||||
<field id="status" xsi:type="AttributeEnum">
|
||||
<sort_type>rank</sort_type>
|
||||
<values>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="production">
|
||||
<code>production</code>
|
||||
<rank>20</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
</value>
|
||||
<value id="obsolete">
|
||||
<code>obsolete</code>
|
||||
<rank>30</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
</values>
|
||||
<sql>status</sql>
|
||||
@@ -1140,9 +1122,6 @@ public function PrefillSearchForm(&$aContextParam)
|
||||
<attribute id="organization_name"/>
|
||||
</attributes>
|
||||
</reconciliation>
|
||||
<obsolescence>
|
||||
<condition><![CDATA[status='obsolete']]></condition>
|
||||
</obsolescence>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="name" xsi:type="AttributeString">
|
||||
@@ -1194,32 +1173,17 @@ public function PrefillSearchForm(&$aContextParam)
|
||||
<field id="status" xsi:type="AttributeEnum">
|
||||
<sort_type>rank</sort_type>
|
||||
<values>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="production">
|
||||
<code>production</code>
|
||||
<rank>20</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
</value>
|
||||
<value id="obsolete">
|
||||
<code>obsolete</code>
|
||||
<rank>30</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
</values>
|
||||
<sql>status</sql>
|
||||
@@ -1584,9 +1548,6 @@ public function PrefillSearchForm(&$aContextParam)
|
||||
<attribute id="service_name"/>
|
||||
</attributes>
|
||||
</reconciliation>
|
||||
<obsolescence>
|
||||
<condition><![CDATA[status='obsolete']]></condition>
|
||||
</obsolescence>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="name" xsi:type="AttributeString">
|
||||
@@ -1635,32 +1596,17 @@ public function PrefillSearchForm(&$aContextParam)
|
||||
<field id="status" xsi:type="AttributeEnum">
|
||||
<sort_type>rank</sort_type>
|
||||
<values>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="production">
|
||||
<code>production</code>
|
||||
<rank>20</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>10</rank>
|
||||
</value>
|
||||
<value id="obsolete">
|
||||
<code>obsolete</code>
|
||||
<rank>30</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
</values>
|
||||
<sql>status</sql>
|
||||
|
||||
@@ -1486,29 +1486,14 @@
|
||||
<value id="draft">
|
||||
<code>draft</code>
|
||||
<rank>10</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="published">
|
||||
<code>published</code>
|
||||
<rank>20</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="obsolete">
|
||||
<code>obsolete</code>
|
||||
<rank>30</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
</values>
|
||||
<sql>status</sql>
|
||||
|
||||
@@ -12,7 +12,7 @@ SetupWebPage::AddModule(
|
||||
// Setup
|
||||
//
|
||||
'dependencies' => [
|
||||
'itop-structure/2.7.1 || itop-portal/3.0.0', // itop-portal : module_design_itop_design->module_designs->itop-portal
|
||||
'itop-structure/2.7.1',
|
||||
],
|
||||
'mandatory' => false,
|
||||
'visible' => true,
|
||||
|
||||
@@ -43,38 +43,18 @@
|
||||
<value id="production">
|
||||
<code>production</code>
|
||||
<rank>30</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="implementation">
|
||||
<code>implementation</code>
|
||||
<rank>20</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="stock">
|
||||
<code>stock</code>
|
||||
<rank>10</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-neutral-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-neutral-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
<value id="obsolete">
|
||||
<code>obsolete</code>
|
||||
<rank>40</rank>
|
||||
<style>
|
||||
<main_color>$ibo-lifecycle-frozen-state-primary-color</main_color>
|
||||
<complementary_color>$ibo-lifecycle-frozen-state-secondary-color</complementary_color>
|
||||
<decoration_classes/>
|
||||
</style>
|
||||
</value>
|
||||
</values>
|
||||
<sql>status</sql>
|
||||
|
||||
@@ -40,8 +40,6 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
|
||||
'Class:AuditRule/Attribute:name+' => 'Krátký název pro toto pravidlo',
|
||||
'Class:AuditRule/Attribute:description' => 'Popis pravidla',
|
||||
'Class:AuditRule/Attribute:description+' => 'Dlouhý popis tohoto pravidla auditu',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => 'Dotaz ke spuštění',
|
||||
'Class:AuditRule/Attribute:query+' => 'OQL výraz ke spuštění',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Interpretace',
|
||||
@@ -54,9 +52,7 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
|
||||
'Class:AuditRule/Attribute:category_id+' => 'Kategorie pro toto pravidlo',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Kategorie',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Název kategorie pro toto pravidlo',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
]);
|
||||
|
||||
//
|
||||
// Class: AuditDomain
|
||||
@@ -168,11 +164,9 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Neaktivní',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Uživatelské jméno musí být jedinečné - "%1s" je již použito.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'Uživateli musí být přidělen alespoň jeden profil.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Profil "%1$s" nemůže být přidán, byl by mu odepřen přístup do backoffice',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Změna není povolena pro vašeho vlastního uživatele',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Přístupné organizace musí obsahovat organizaci uživatele.',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'Aktuální seznam profilů neposkytuje dostatečná přístupová práva (uživatele již nelze upravovat)',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'Profil Portal power user neposkytuje dostatečná přístupová práva (je třeba přidat jiný profil)',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'Uživatel musí být přiřazen minimálně do jedné organizace.',
|
||||
|
||||
@@ -40,8 +40,6 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
|
||||
'Class:AuditRule/Attribute:name+' => 'Kort navn for denne regel',
|
||||
'Class:AuditRule/Attribute:description' => 'Audit-regel beskrivelse',
|
||||
'Class:AuditRule/Attribute:description+' => 'Udførlig beskrivelse af denne Audit-regel',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => 'Søgning at udføre',
|
||||
'Class:AuditRule/Attribute:query+' => 'Den OQL forespørgsel, der skal udføres',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Gyldige objekter?',
|
||||
@@ -54,8 +52,6 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
|
||||
'Class:AuditRule/Attribute:category_id+' => 'Kategori for denne regel',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Kategori',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Kategorinavn for denne regel',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -168,12 +164,10 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Disabled~~',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Login skal være entydig - "%1s" er allerede i brug.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'Mindst en profil skal knyttes til denne bruger.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Profile "%1$s" cannot be added it will deny the access to backoffice~~',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Changing status is not allowed for your own User~~',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Allowed organizations must contain User organization~~',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'You cannot remove your own rights to edit Users~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'The current list of profiles does not give sufficient access rights (Users are not modifiable anymore)~~',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)~~',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'At least one organization must be assigned to this user.~~',
|
||||
'Class:User/Error:OrganizationNotAllowed' => 'Organization not allowed.~~',
|
||||
|
||||
@@ -40,8 +40,6 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
|
||||
'Class:AuditRule/Attribute:name+' => 'Kurzname für diese Regel',
|
||||
'Class:AuditRule/Attribute:description' => 'Beschreibung der Audit-Regel',
|
||||
'Class:AuditRule/Attribute:description+' => 'Ausführliche Beschreibung dieser Audit-Regel',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => 'Durchzuführende Abfrage',
|
||||
'Class:AuditRule/Attribute:query+' => 'Die auszuführende OQL-Abfrage',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Gültiges Objekt?',
|
||||
@@ -54,8 +52,6 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
|
||||
'Class:AuditRule/Attribute:category_id+' => 'Kategorie für diese Regel',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Kategorie',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Kategoriename für diese Regel',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -167,11 +163,9 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Inaktiv',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Login-Namen müssen unterschiedlich sein - "%1s" benutzt diesen Login-Name bereits.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'Mindestens ein Profil muss diesem Benutzer zugewiesen sein.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Profil "%1$s" kann nicht hinzugefügt werde, es verhindert den Zugriff auf das Backoffice.',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Statusänderungen sind für den eigenen Benutzer nicht erlaubt.',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Die Organisation des Benutzers muss in den erlaubten Organisationen enthalten sein.',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'Die aktuelle Liste an Profilen vergibt unzureichende Berechtigungen (Benutzer können nicht mehr geändert werden)',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'Das Profil des Portal-Power-Benutzers hat nicht ausreichend Zugriffsrechte (ein weiteres Profil muss hinzugefügt werden)',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'Mindestens eine Organisation muss diesem Benutzer zugewiesen sein.',
|
||||
|
||||
@@ -50,9 +50,7 @@ It is applied on the scope of objects defined by the audit category',
|
||||
'Class:AuditRule/Attribute:name' => 'Rule name',
|
||||
'Class:AuditRule/Attribute:name+' => 'Short name for this rule',
|
||||
'Class:AuditRule/Attribute:description' => 'Description',
|
||||
'Class:AuditRule/Attribute:description+' => 'What is checked?',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...',
|
||||
'Class:AuditRule/Attribute:description+' => 'What is checked? How should it be fixed? Who should do it? ...',
|
||||
'Class:AuditRule/Attribute:query' => 'Query to run',
|
||||
'Class:AuditRule/Attribute:query+' => 'The OQL expression to run. Returned classes must be aligned with those of the category\'s scope',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Returned objects: ',
|
||||
@@ -67,8 +65,6 @@ It is applied on the scope of objects defined by the audit category',
|
||||
'Class:AuditRule/Attribute:category_id+' => 'The category of this rule',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Category name',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Name of the category of this rule',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -177,17 +173,15 @@ Dict::Add('EN US', 'English', 'English', [
|
||||
'Class:User/Attribute:allowed_org_list+' => 'The end user is allowed to see data belonging to the following organizations. If no organization is specified, there is no restriction.',
|
||||
'Class:User/Attribute:status' => 'Status',
|
||||
'Class:User/Attribute:status+' => 'Whether the user account is enabled or disabled.',
|
||||
'Class:User/Attribute:status/Value:enabled' => 'Enabled',
|
||||
'Class:User/Attribute:status/Value:enabled' => 'Enabled',
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Disabled',
|
||||
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Login must be unique - "%1$s" is already being used.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'At least one profile must be assigned to this user.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Profile "%1$s" cannot be added it will deny the access to backoffice',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Changing status is not allowed for your own User',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Allowed organizations must contain User organization',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'You cannot remove your own rights to edit Users',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'The current list of profiles does not give sufficient access rights (Users are not modifiable anymore)',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'At least one organization must be assigned to this user.',
|
||||
'Class:User/Error:OrganizationNotAllowed' => 'Organization not allowed.',
|
||||
|
||||
@@ -50,9 +50,7 @@ It is applied on the scope of objects defined by the audit category',
|
||||
'Class:AuditRule/Attribute:name' => 'Rule name',
|
||||
'Class:AuditRule/Attribute:name+' => 'Short name for this rule',
|
||||
'Class:AuditRule/Attribute:description' => 'Description',
|
||||
'Class:AuditRule/Attribute:description+' => 'What is checked?~~',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:description+' => 'What is checked? How should it be fixed? Who should do it? ...',
|
||||
'Class:AuditRule/Attribute:query' => 'Query to run',
|
||||
'Class:AuditRule/Attribute:query+' => 'The OQL expression to run. Returned classes must be aligned with those of the category\'s scope',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Returned objects: ',
|
||||
@@ -67,8 +65,6 @@ It is applied on the scope of objects defined by the audit category',
|
||||
'Class:AuditRule/Attribute:category_id+' => 'The category of this rule',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Category name',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Name of the category of this rule',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -182,11 +178,9 @@ Dict::Add('EN GB', 'British English', 'British English', [
|
||||
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Login must be unique - "%1$s" is already being used.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'At least one profile must be assigned to this user.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Profile "%1$s" cannot be added as it will deny access to the back office.',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Changing status is not allowed for your own User',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Allowed organisations must contain User organisation',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'The current list of profiles does not give sufficient access rights (Users are not modifiable any more)',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'At least one organisation must be assigned to this user.',
|
||||
|
||||
@@ -38,8 +38,6 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
|
||||
'Class:AuditRule/Attribute:name+' => 'Nombre corto para esta regla',
|
||||
'Class:AuditRule/Attribute:description' => 'Descripción de regla de auditoría',
|
||||
'Class:AuditRule/Attribute:description+' => 'Descripción larga para esta regla de auditoría',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => 'Consulta a Ejecutar',
|
||||
'Class:AuditRule/Attribute:query+' => 'Expresión OQL a ejecutar',
|
||||
'Class:AuditRule/Attribute:valid_flag' => '¿Objetos Válidos?',
|
||||
@@ -52,8 +50,6 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
|
||||
'Class:AuditRule/Attribute:category_id+' => 'La categoría para esta regla',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Categoría',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Nombre de la categoría para esta regla',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -166,11 +162,9 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Deshabilitado',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Usuario debe ser único - "%1s" ya se encuentra en uso.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'Al menos un Perfil debe ser asignado a este usuario.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'No se puede agregar el perfil "%1$s"; denegará el acceso al backoffice',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Cambiar estatus no está permitido para su propio usuario',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Las organizaciones permitidas deben contener una organización de usuario',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'La lista actual de perfiles no otorga suficientes permisos de acceso (los usuarios ya no son modificables)',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'El perfil de usuario avanzado del Portal no otorga suficientes derechos de acceso (se debe agregar otro perfil)',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'Al menos una organización debe ser asignada a este usuario.',
|
||||
|
||||
@@ -41,9 +41,7 @@ Elle s\'applique à tous les objets dans le périmètre de sa catégorie d\'audi
|
||||
'Class:AuditRule/Attribute:name' => 'Nom',
|
||||
'Class:AuditRule/Attribute:name+' => 'Une vérification particulière',
|
||||
'Class:AuditRule/Attribute:description' => 'Description',
|
||||
'Class:AuditRule/Attribute:description+' => 'Qu\'est ce qu\'on vérifie ?',
|
||||
'Class:AuditRule/Attribute:process' => 'Processus de correction',
|
||||
'Class:AuditRule/Attribute:process+' => 'Comment le corriger ? Qui doit le faire ? ...',
|
||||
'Class:AuditRule/Attribute:description+' => 'Qu\'est ce qu\'on vérifie ? Comment le corriger ? Qui doit le faire ? ...',
|
||||
'Class:AuditRule/Attribute:query' => 'Requête',
|
||||
'Class:AuditRule/Attribute:query+' => 'Requête OQL à executer. Les classes retournées doivent être cohérentes avec celles définies dans le périmètre de la catégorie',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Objets retournés :',
|
||||
@@ -58,8 +56,6 @@ Elle s\'applique à tous les objets dans le périmètre de sa catégorie d\'audi
|
||||
'Class:AuditRule/Attribute:category_id+' => '',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Nom de la catégorie',
|
||||
'Class:AuditRule/Attribute:category_name+' => '',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Responsable',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Personne ou équipe responsable de la correction des erreurs détectées par cette règle',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -168,17 +164,15 @@ Dict::Add('FR FR', 'French', 'Français', [
|
||||
'Class:User/Attribute:allowed_org_list' => 'Organisations permises',
|
||||
'Class:User/Attribute:allowed_org_list+' => 'L\'utilisateur a le droit de voir les données des organisations listées ici. Si aucune organisation n\'est spécifiée, alors aucune restriction ne s\'applique.',
|
||||
'Class:User/Attribute:status' => 'Etat',
|
||||
'Class:User/Attribute:status+' => 'Est-ce que ce compte utilisateur est actif, ou non ?',
|
||||
'Class:User/Attribute:status+' => 'Est-ce que ce compte utilisateur est actif, ou non?',
|
||||
'Class:User/Attribute:status/Value:enabled' => 'Actif',
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Désactivé',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Le login doit être unique - "%1s" est déjà utilisé.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'L\'utilisateur doit avoir au moins un profil.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Le profil "%1$s" ne peut pas être donné aux Administrateurs, SuperUsers et REST Services Users',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Le profil "%1$s" ne peux pas être ajouté à son propre utilisateur, il interdit l\'accès à la console',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Impossible de changer l\'état de son propre utilisateur',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Les organisations permises doivent contenir l\'organisation de l\'utilisateur',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'Vous ne pouvez pas supprimer votre propre profil Administrateur. Demandez à un autre Administrateur de le faire pour vous',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'Vous ne pouvez pas supprimer vos propres droits de modification des utilisateurs.',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'Les profils existants ne permettent pas de modifier les utilisateurs',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'Le profil Portal power user ne donne pas suffisamment de droits à l\'utilisateur (un autre profil doit être ajouté)',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'L\'utilisateur doit avoir au moins une organisation.',
|
||||
'Class:User/Error:OrganizationNotAllowed' => 'Organisation non autorisée.',
|
||||
|
||||
@@ -40,8 +40,6 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', [
|
||||
'Class:AuditRule/Attribute:name+' => '',
|
||||
'Class:AuditRule/Attribute:description' => 'Leírás',
|
||||
'Class:AuditRule/Attribute:description+' => '',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => 'Lekérdezés',
|
||||
'Class:AuditRule/Attribute:query+' => '',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Érvényes objektum?',
|
||||
@@ -54,8 +52,6 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', [
|
||||
'Class:AuditRule/Attribute:category_id+' => '',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Kategórianév',
|
||||
'Class:AuditRule/Attribute:category_name+' => '',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -168,11 +164,9 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Letiltott',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'A felhasználónévnek egyedinek kell lennie - "%1s" már létezik.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'Legalább egy profilt a felhasználóhoz kell rendelni.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'A "%1$s" profil nem adható hozzá, le lesz tiltva',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'A saját felhasználó státuszának cseréje nem engedélyezett',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Az engedélyezett szervezeteknek tartalmazniuk kell a felhasználói szervezetet',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'A profilok jelenlegi listája nem ad elegendő hozzáférési jogot (a felhasználók már nem módosíthatók)',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)~~',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'A felhasználóhoz legalább egy szervezeti egységet hozzá kell rendelni',
|
||||
|
||||
@@ -40,8 +40,6 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [
|
||||
'Class:AuditRule/Attribute:name+' => '',
|
||||
'Class:AuditRule/Attribute:description' => 'Descrizione della regola di Audit',
|
||||
'Class:AuditRule/Attribute:description+' => 'Descrizione dettagliata per questa regola di audit ',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => 'Query da eseguire',
|
||||
'Class:AuditRule/Attribute:query+' => 'Espressio OQL da eseguire',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Oggetti validi?',
|
||||
@@ -54,8 +52,6 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [
|
||||
'Class:AuditRule/Attribute:category_id+' => 'Categoria per questa regola',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Categoria',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Nome della categoria per questa regola',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -168,11 +164,9 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Disabilitato',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Il Login deve essere unico - "%1s" già in uso',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'È necessario almeno un profilo assegnato all\'utente.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Il profilo "%1$s" non può essere aggiunto poiché nega l\'accesso al back office.',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'La modifica dello stato non è consentita per il proprio utente.',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Le organizzazioni consentite devono includere l\'organizzazione dell\'utente.',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'L\'elenco attuale dei profili non conferisce diritti di accesso sufficienti (gli utenti non sono più modificabili).',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'Il profilo utente con poteri del portale non concede diritti di accesso sufficienti (deve essere aggiunto un altro profilo)',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'È necessario assegnare almeno un\'organizzazione a questo utente.',
|
||||
|
||||
@@ -40,8 +40,6 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
|
||||
'Class:AuditRule/Attribute:name+' => 'ルールの短縮名',
|
||||
'Class:AuditRule/Attribute:description' => '監査ルール説明',
|
||||
'Class:AuditRule/Attribute:description+' => 'この監査ルールの長い説明',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => '実行するクエリ',
|
||||
'Class:AuditRule/Attribute:query+' => '実行するOQL式',
|
||||
'Class:AuditRule/Attribute:valid_flag' => '有効なオブジェクト',
|
||||
@@ -54,8 +52,6 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
|
||||
'Class:AuditRule/Attribute:category_id+' => 'このルールのカテゴリ',
|
||||
'Class:AuditRule/Attribute:category_name' => 'カテゴリ',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'このルールのカテゴリ名',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -168,12 +164,10 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Disabled~~',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'ログイン名は一意でないといけません。- "%1s" はすでに使われています。',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => '少なくとも1件のプロフィールがこのユーザに指定されなければなりません。',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Profile "%1$s" cannot be added it will deny the access to backoffice~~',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Changing status is not allowed for your own User~~',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Allowed organizations must contain User organization~~',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'You cannot remove your own rights to edit Users~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'The current list of profiles does not give sufficient access rights (Users are not modifiable anymore)~~',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)~~',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'At least one organization must be assigned to this user.~~',
|
||||
'Class:User/Error:OrganizationNotAllowed' => 'Organization not allowed.~~',
|
||||
|
||||
@@ -40,8 +40,6 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [
|
||||
'Class:AuditRule/Attribute:name+' => 'Naam van de regel',
|
||||
'Class:AuditRule/Attribute:description' => 'Beschrijving',
|
||||
'Class:AuditRule/Attribute:description+' => 'Uitgebreide beschrijving van deze Auditregel',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => 'Query om uit te voeren',
|
||||
'Class:AuditRule/Attribute:query+' => 'De OQL-expressie voor het uitvoeren',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Geldige objecten?',
|
||||
@@ -54,8 +52,6 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [
|
||||
'Class:AuditRule/Attribute:category_id+' => 'De categorie voor deze regel',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Categorie',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Naam van de categorie voor deze regel',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -168,11 +164,9 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Uitgeschakeld',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Login moet uniek zijn - "%1s" is al in gebruik',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'Minstens één profiel moet toegewezen zijn aan deze gebruiker',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Profiel "%1$s" kan niet toegevoegd worden omdat het de toegang tot de backoffice zou ontzeggen.',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Je kan de status voor je eigen gebruikersaccount niet wijzigen.',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'De toegestande organisaties moeten minstens de organisatie bevatten waartoe de gebruikersaccount behoort.',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'De huidige lijst van profielen heeft niet voldoende toegangsrechten (gebruikersaccount zijn niet meer wijzigbaar).',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)~~',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'Minstens één organisatie moet toegewezen zijn aan deze gebruiker',
|
||||
|
||||
@@ -40,8 +40,6 @@ Dict::Add('PL PL', 'Polish', 'Polski', [
|
||||
'Class:AuditRule/Attribute:name+' => 'Krótka nazwa reguły',
|
||||
'Class:AuditRule/Attribute:description' => 'Opis reguły audytu',
|
||||
'Class:AuditRule/Attribute:description+' => 'Długi opis reguły inspekcji',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => 'Zapytanie do wykonania',
|
||||
'Class:AuditRule/Attribute:query+' => 'Wyrażenie OQL do wykonania',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Prawidłowe obiekty?',
|
||||
@@ -54,8 +52,6 @@ Dict::Add('PL PL', 'Polish', 'Polski', [
|
||||
'Class:AuditRule/Attribute:category_id+' => 'Kategoria dla reguły',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Kategoria',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Nazwa kategorii dla reguły',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -168,11 +164,9 @@ Dict::Add('PL PL', 'Polish', 'Polski', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Wyłączone',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Login musi być unikatowy - "%1s" jest już używany.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'Do użytkownika musi być przypisany co najmniej jeden profil.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Nie można dodać profilu "%1$s" nie ma on dostępu do zaplecza',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Zmiana statusu nie jest dozwolona dla własnego użytkownika',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Dozwolone organizacje muszą zawierać organizację użytkownika',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'Aktualna lista profili nie daje wystarczających praw dostępu (Użytkowników nie można już modyfikować)',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'Profil użytkownika zaawansowanego Portalu nie zapewnia wystarczających praw dostępu (należy dodać kolejny profil)',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'Do użytkownika musi być przypisana co najmniej jedna organizacja.',
|
||||
|
||||
@@ -40,8 +40,6 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
|
||||
'Class:AuditRule/Attribute:name+' => 'Nome curto para esta regra',
|
||||
'Class:AuditRule/Attribute:description' => 'Descrição',
|
||||
'Class:AuditRule/Attribute:description+' => 'Descrição longa para essa regra',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => 'Executar consulta',
|
||||
'Class:AuditRule/Attribute:query+' => 'Executar a expressão OQL',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Objetos válidos?',
|
||||
@@ -54,8 +52,6 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
|
||||
'Class:AuditRule/Attribute:category_id+' => 'A categoria para esta regra',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Categoria',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Nome da categoria para essa regra',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -168,11 +164,9 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Inativa',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Login deve ser único - "%1s" já existe',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'Pelo menos um perfil deve ser atribuído a esse usuário',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'O perfil "%1$s" não pôde ser adicionado, ele negará o acesso ao backoffice',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Alterar o status da conta não é permitido para o seu próprio usuário',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'As organizações permitidas devem conter apenas usuários pertencentes a organização',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'A lista atual de perfis não fornece permissões de acesso suficientes (os usuários não são mais modificáveis)',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)~~',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'Pelo menos uma organização deve ser atribuída a esse usuário',
|
||||
|
||||
@@ -41,8 +41,6 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Class:AuditRule/Attribute:name+' => 'Краткое название этого правила',
|
||||
'Class:AuditRule/Attribute:description' => 'Описание правила аудита',
|
||||
'Class:AuditRule/Attribute:description+' => 'Полное описание этого правила аудита',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => 'Запрос для выполнения',
|
||||
'Class:AuditRule/Attribute:query+' => 'OQL выражение, выполняющее проверку набора объектов категории аудита',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Валидные объекты?',
|
||||
@@ -55,8 +53,6 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Class:AuditRule/Attribute:category_id+' => 'Категория для этого правила',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Категория',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Категория для этого правила',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -169,12 +165,10 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Отключен',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Логин должен быть уникальным - "%1s" уже используется.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'Как минимум один профиль должен быть назначен данному пользователю.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Profile "%1$s" cannot be added it will deny the access to backoffice~~',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Changing status is not allowed for your own User~~',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Allowed organizations must contain User organization~~',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'You cannot remove your own rights to edit Users~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'The current list of profiles does not give sufficient access rights (Users are not modifiable anymore)~~',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)~~',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'Этому пользователю должна быть назначена хотя бы одна организация.',
|
||||
'Class:User/Error:OrganizationNotAllowed' => 'Организация не разрешена.',
|
||||
|
||||
@@ -41,9 +41,7 @@ It is applied on the scope of objects defined by the audit category~~',
|
||||
'Class:AuditRule/Attribute:name' => 'Názov pravidla',
|
||||
'Class:AuditRule/Attribute:name+' => 'Short name for this rule~~',
|
||||
'Class:AuditRule/Attribute:description' => 'Popis pravidla auditu',
|
||||
'Class:AuditRule/Attribute:description+' => 'What is checked?~~',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:description+' => 'What is checked? How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => 'Spustenie dopytu',
|
||||
'Class:AuditRule/Attribute:query+' => 'The OQL expression to run. Returned classes must be aligned with those of the category\'s scope~~',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Platný objekt?',
|
||||
@@ -58,8 +56,6 @@ It is applied on the scope of objects defined by the audit category~~',
|
||||
'Class:AuditRule/Attribute:category_id+' => 'The category of this rule~~',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Kategória',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Name of the category of this rule~~',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -172,12 +168,10 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Disabled~~',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Prihlasovacie meno musí byť jedinečné - "%1s" sa už používa.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'Aspoň jeden profil musí byť priradený k profilu.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Profile "%1$s" cannot be added it will deny the access to backoffice~~',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Changing status is not allowed for your own User~~',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Allowed organizations must contain User organization~~',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'You cannot remove your own rights to edit Users~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'The current list of profiles does not give sufficient access rights (Users are not modifiable anymore)~~',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)~~',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'At least one organization must be assigned to this user.~~',
|
||||
'Class:User/Error:OrganizationNotAllowed' => 'Organization not allowed.~~',
|
||||
|
||||
@@ -40,8 +40,6 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [
|
||||
'Class:AuditRule/Attribute:name+' => 'Kural Adı',
|
||||
'Class:AuditRule/Attribute:description' => 'Kural tanımlaması',
|
||||
'Class:AuditRule/Attribute:description+' => 'Kural tanımlaması',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => 'Çalıştırılacak Sorgu',
|
||||
'Class:AuditRule/Attribute:query+' => 'Çalıştırılcak OQL ifadesi',
|
||||
'Class:AuditRule/Attribute:valid_flag' => 'Geçerli nesneler?',
|
||||
@@ -54,8 +52,6 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [
|
||||
'Class:AuditRule/Attribute:category_id+' => 'Kuralın kategorisi',
|
||||
'Class:AuditRule/Attribute:category_name' => 'Kategori',
|
||||
'Class:AuditRule/Attribute:category_name+' => 'Kural için kategori adı',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -168,12 +164,10 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [
|
||||
'Class:User/Attribute:status/Value:disabled' => 'Disabled~~',
|
||||
'Class:User/Error:LoginMustBeUnique' => 'Kullanıcı adı tekil olmalı - "%1s" mevcut bir kullanıcıya ait.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'En az bir profil kullanıcıya atanmalı',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => 'Profile "%1$s" cannot be added it will deny the access to backoffice~~',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Changing status is not allowed for your own User~~',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Allowed organizations must contain User organization~~',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'You cannot remove your own rights to edit Users~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'The current list of profiles does not give sufficient access rights (Users are not modifiable anymore)~~',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)~~',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'At least one organization must be assigned to this user.~~',
|
||||
'Class:User/Error:OrganizationNotAllowed' => 'Organization not allowed.~~',
|
||||
|
||||
@@ -42,8 +42,6 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:AuditRule/Attribute:name+' => '规则名称',
|
||||
'Class:AuditRule/Attribute:description' => '描述',
|
||||
'Class:AuditRule/Attribute:description+' => '检查什么? 如何修复? 谁去做? ...',
|
||||
'Class:AuditRule/Attribute:process' => 'Correction process~~',
|
||||
'Class:AuditRule/Attribute:process+' => 'How should it be fixed? Who should do it? ...~~',
|
||||
'Class:AuditRule/Attribute:query' => '要运行的查询',
|
||||
'Class:AuditRule/Attribute:query+' => '要运行的OQL表达式',
|
||||
'Class:AuditRule/Attribute:valid_flag' => '是否有效?',
|
||||
@@ -56,8 +54,6 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:AuditRule/Attribute:category_id+' => '该规则对应的类别',
|
||||
'Class:AuditRule/Attribute:category_name' => '类别',
|
||||
'Class:AuditRule/Attribute:category_name+' => '该规则对应的类名称',
|
||||
'Class:AuditRule/Attribute:contact_id' => 'Owner~~',
|
||||
'Class:AuditRule/Attribute:contact_id+' => 'Team or person in charge of fixing the errors detected by this rule~~',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -170,11 +166,9 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:User/Attribute:status/Value:disabled' => '停用',
|
||||
'Class:User/Error:LoginMustBeUnique' => '登录名必须唯一 - "%1s" 已经被使用.',
|
||||
'Class:User/Error:AtLeastOneProfileIsNeeded' => '必须指定至少一个角色给此用户.',
|
||||
'Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice' => 'Profile "%1$s" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)~~',
|
||||
'Class:User/Error:ProfileNotAllowed' => '无法添加角色 "%1$s" 因为这将导致禁止访问后台',
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => '不允许更改您自己用户的状态',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => '允许访问组织必须包含用户组织',
|
||||
'Class:User/Error:AdminProfileCannotBeRemovedBySelf' => 'You cannot remove your own Administrator profile. Ask another Administrator to do it for you~~',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => '当前指定的角色列表没有提供足够的访问权限 (用户将无法被修改)',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)~~',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => '必须为此用户指定一个组织.',
|
||||
|
||||
@@ -14,10 +14,7 @@ if (PHP_VERSION_ID < 50600) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
$err,
|
||||
E_USER_ERROR
|
||||
);
|
||||
throw new RuntimeException($err);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
@@ -26,12 +26,23 @@ use Composer\Semver\VersionParser;
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
|
||||
* @internal
|
||||
*/
|
||||
private static $selfDir = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $installedIsLocalDir;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
@@ -309,6 +320,24 @@ class InstalledVersions
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
|
||||
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||
// all installed packages for example
|
||||
self::$installedIsLocalDir = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private static function getSelfDir()
|
||||
{
|
||||
if (self::$selfDir === null) {
|
||||
self::$selfDir = strtr(__DIR__, '\\', '/');
|
||||
}
|
||||
|
||||
return self::$selfDir;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,7 +354,9 @@ class InstalledVersions
|
||||
$copiedLocalDir = false;
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
$selfDir = self::getSelfDir();
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
@@ -333,11 +364,14 @@ class InstalledVersions
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
self::$installedByVendor[$vendorDir] = $required;
|
||||
$installed[] = $required;
|
||||
if (strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||
self::$installed = $required;
|
||||
$copiedLocalDir = true;
|
||||
self::$installedIsLocalDir = true;
|
||||
}
|
||||
}
|
||||
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||
$copiedLocalDir = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ return array(
|
||||
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
|
||||
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
|
||||
'Pelago\\Emogrifier\\' => array($vendorDir . '/pelago/emogrifier/src'),
|
||||
'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src', $vendorDir . '/league/oauth2-google/src'),
|
||||
'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-google/src', $vendorDir . '/league/oauth2-client/src'),
|
||||
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
|
||||
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
|
||||
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
|
||||
|
||||
@@ -340,8 +340,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
),
|
||||
'League\\OAuth2\\Client\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/league/oauth2-client/src',
|
||||
1 => __DIR__ . '/..' . '/league/oauth2-google/src',
|
||||
0 => __DIR__ . '/..' . '/league/oauth2-google/src',
|
||||
1 => __DIR__ . '/..' . '/league/oauth2-client/src',
|
||||
),
|
||||
'GuzzleHttp\\Psr7\\' =>
|
||||
array (
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
'name' => 'combodo/itop',
|
||||
'pretty_version' => 'dev-develop',
|
||||
'version' => 'dev-develop',
|
||||
'reference' => '469afdb2f9aea1b6e078a2a5bb12f09a969d60e0',
|
||||
'reference' => '19d062aa830b6d6c7d17ac4046fc9ee2c5e3fab1',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
@@ -22,7 +22,7 @@
|
||||
'combodo/itop' => array(
|
||||
'pretty_version' => 'dev-develop',
|
||||
'version' => 'dev-develop',
|
||||
'reference' => '469afdb2f9aea1b6e078a2a5bb12f09a969d60e0',
|
||||
'reference' => '19d062aa830b6d6c7d17ac4046fc9ee2c5e3fab1',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
||||
@@ -36,8 +36,7 @@ if ($issues) {
|
||||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues),
|
||||
E_USER_ERROR
|
||||
throw new \RuntimeException(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
class SetupDBBackup extends DBBackup
|
||||
{
|
||||
protected function LogInfo($sMsg)
|
||||
{
|
||||
SetupLog::Ok('Info - '.$sMsg);
|
||||
}
|
||||
|
||||
protected function LogError($sMsg)
|
||||
{
|
||||
SetupLog::Ok('Error - '.$sMsg);
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past
|
||||
/**
|
||||
* Main program
|
||||
*/
|
||||
$sOperation = utils::ReadParam('operation', '');
|
||||
$sOperation = Utils::ReadParam('operation', '');
|
||||
try {
|
||||
SetupUtils::CheckSetupToken();
|
||||
|
||||
@@ -139,6 +139,7 @@ try {
|
||||
ini_set('display_startup_errors', true);
|
||||
|
||||
require_once(APPROOT.'/setup/wizardcontroller.class.inc.php');
|
||||
require_once(APPROOT.'/setup/wizardsteps.class.inc.php');
|
||||
|
||||
$sClass = utils::ReadParam('step_class', '');
|
||||
$sState = utils::ReadParam('step_state', '');
|
||||
@@ -163,7 +164,7 @@ try {
|
||||
break;
|
||||
|
||||
case 'toggle_use_symbolic_links':
|
||||
$sUseSymbolicLinks = utils::ReadParam('bUseSymbolicLinks', false);
|
||||
$sUseSymbolicLinks = Utils::ReadParam('bUseSymbolicLinks', false);
|
||||
$bUseSymbolicLinks = ($sUseSymbolicLinks === 'true');
|
||||
MFCompiler::SetUseSymbolicLinksFlag($bUseSymbolicLinks);
|
||||
echo "toggle useSymbolicLinks flag : $bUseSymbolicLinks";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,8 +21,8 @@
|
||||
use Combodo\iTop\Application\Branding;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Application\WebPage\Page;
|
||||
use Combodo\iTop\DesignDocument;
|
||||
use Combodo\iTop\DesignElement;
|
||||
use Combodo\iTop\DesignDocument;
|
||||
|
||||
require_once(APPROOT.'setup/setuputils.class.inc.php');
|
||||
require_once(APPROOT.'setup/modelfactory.class.inc.php');
|
||||
@@ -3357,8 +3357,6 @@ EOF;
|
||||
|
||||
$bDataXmlPrecompiledFileExists = false;
|
||||
clearstatcache();
|
||||
|
||||
$iDataXmlFileLastModified = 0;
|
||||
if (!empty($sPrecompiledFileUri)) {
|
||||
$sDataXmlProvidedPrecompiledFile = $sTempTargetDir.DIRECTORY_SEPARATOR.$sPrecompiledFileUri;
|
||||
$bDataXmlPrecompiledFileExists = file_exists($sDataXmlProvidedPrecompiledFile) ;
|
||||
|
||||
@@ -34,7 +34,7 @@ require_once(APPROOT.'/application/startup.inc.php');
|
||||
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
|
||||
LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
|
||||
|
||||
$sOperation = utils::ReadParam('operation', 'step1');
|
||||
$sOperation = Utils::ReadParam('operation', 'step1');
|
||||
$oP = new SetupPage('iTop email test utility');
|
||||
|
||||
// Although this page doesn't expose sensitive info, with it we can send multiple emails
|
||||
@@ -208,7 +208,7 @@ function DisplayStep2(SetupPage $oP, $sFrom, $sTo)
|
||||
$oP->add("<p>Sending an email to '".htmlentities($sTo, ENT_QUOTES, 'utf-8')."'... (From: '".htmlentities($sFrom, ENT_QUOTES, 'utf-8')."')</p>\n");
|
||||
$oP->add("<form method=\"post\">\n");
|
||||
|
||||
$oEmail = new EMail();
|
||||
$oEmail = new Email();
|
||||
$oEmail->SetRecipientTO($sTo);
|
||||
$oEmail->SetRecipientFrom($sFrom);
|
||||
$oEmail->SetSubject("Test iTop");
|
||||
@@ -256,8 +256,8 @@ try {
|
||||
|
||||
case 'step2':
|
||||
$oP->no_cache();
|
||||
$sTo = utils::ReadParam('to', '', false, 'raw_data');
|
||||
$sFrom = utils::ReadParam('from', '', false, 'raw_data');
|
||||
$sTo = Utils::ReadParam('to', '', false, 'raw_data');
|
||||
$sFrom = Utils::ReadParam('from', '', false, 'raw_data');
|
||||
DisplayStep2($oP, $sFrom, $sTo);
|
||||
break;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Setup\FeatureRemoval;
|
||||
|
||||
use ContextTag;
|
||||
use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
use IssueLog;
|
||||
use MetaModel;
|
||||
use SetupLog;
|
||||
|
||||
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
|
||||
|
||||
abstract class AbstractSetupAudit
|
||||
{
|
||||
protected bool $bClassesInitialized = false;
|
||||
protected array $aClassesBefore = [];
|
||||
protected array $aClassesAfter = [];
|
||||
protected array $aRemovedClasses = [];
|
||||
protected array $aFinalClassesToCleanup = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
abstract public function ComputeClasses(): void;
|
||||
|
||||
public function GetRemovedClasses(): array
|
||||
{
|
||||
$this->ComputeClasses();
|
||||
|
||||
if (count($this->aRemovedClasses) == 0) {
|
||||
if (count($this->aClassesBefore) == 0) {
|
||||
return $this->aRemovedClasses;
|
||||
}
|
||||
|
||||
if (count($this->aClassesAfter) == 0) {
|
||||
return $this->aRemovedClasses;
|
||||
}
|
||||
|
||||
$aExtensionsNames = array_diff($this->aClassesBefore, $this->aClassesAfter);
|
||||
$this->aRemovedClasses = [];
|
||||
$aClasses = array_values($aExtensionsNames);
|
||||
sort($aClasses);
|
||||
|
||||
foreach ($aClasses as $i => $sClass) {
|
||||
$this->aRemovedClasses[] = $sClass;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->aRemovedClasses;
|
||||
}
|
||||
|
||||
public function GetIssues(bool $bStopDataCheckAtFirstIssue = false): array
|
||||
{
|
||||
$this->aFinalClassesToCleanup = [];
|
||||
|
||||
foreach ($this->GetRemovedClasses() as $sClass) {
|
||||
if (MetaModel::IsAbstract($sClass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!MetaModel::IsStandaloneClass($sClass)) {
|
||||
$iCount = $this->Count($sClass);
|
||||
$this->aFinalClassesToCleanup[$sClass] = $iCount;
|
||||
if ($bStopDataCheckAtFirstIssue && $iCount > 0) {
|
||||
//setup envt: should raise issue ASAP
|
||||
$this->LogInfoWithProperLogger("Setup audit found data to cleanup", null, $this->aFinalClassesToCleanup);
|
||||
return $this->aFinalClassesToCleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->LogInfoWithProperLogger("Setup audit found data to cleanup", null, ['data_to_cleanup' => $this->aFinalClassesToCleanup]);
|
||||
return $this->aFinalClassesToCleanup;
|
||||
}
|
||||
|
||||
public function GetDataToCleanupCount(): int
|
||||
{
|
||||
$res = 0;
|
||||
foreach ($this->aFinalClassesToCleanup as $sClass => $iCount) {
|
||||
$res += $iCount;
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
private function Count($sClass): int
|
||||
{
|
||||
$oSearch = DBObjectSearch::FromOQL("SELECT $sClass", []);
|
||||
$oSearch->AllowAllData();
|
||||
$oSet = new DBObjectSet($oSearch);
|
||||
|
||||
return $oSet->Count();
|
||||
}
|
||||
|
||||
//could be shared with others in log APIs ?
|
||||
private function LogInfoWithProperLogger($sMessage, $sChannel = null, $aContext = []): void
|
||||
{
|
||||
if (ContextTag::Check(ContextTag::TAG_SETUP)) {
|
||||
SetupLog::Info($sMessage, $sChannel, $aContext);
|
||||
} else {
|
||||
IssueLog::Info($sMessage, $sChannel, $aContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Setup\FeatureRemoval;
|
||||
|
||||
use Combodo\iTop\Setup\ModuleDependency\Module;
|
||||
use Config;
|
||||
use InstallationChoicesToModuleConverter;
|
||||
use iTopExtensionsMap;
|
||||
use MetaModel;
|
||||
use ModuleDiscovery;
|
||||
use RunTimeEnvironment;
|
||||
use SetupUtils;
|
||||
use utils;
|
||||
|
||||
class DryRemovalRuntimeEnvironment extends RunTimeEnvironment
|
||||
{
|
||||
public const DRY_REMOVAL_AUDIT_ENV = "extension-removal";
|
||||
|
||||
protected array $aExtensionsByCode;
|
||||
|
||||
/**
|
||||
* Toolset for building a run-time environment
|
||||
*
|
||||
* @param string $sEnvironment (e.g. 'test')
|
||||
* @param bool $bAutoCommit (make the target environment directly, or build a temporary one)
|
||||
*/
|
||||
public function __construct($sEnvironment = self::DRY_REMOVAL_AUDIT_ENV, $bAutoCommit = true)
|
||||
{
|
||||
parent::__construct($sEnvironment, $bAutoCommit);
|
||||
$this->aExtensionsByCode = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sSourceEnv
|
||||
* @param array $aExtensionCodesToRemove
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function Prepare(string $sSourceEnv, array $aExtensionCodesToRemove)
|
||||
{
|
||||
|
||||
$sEnv = $this->sFinalEnv;
|
||||
$this->aExtensionsByCode = $aExtensionCodesToRemove;
|
||||
|
||||
$this->Cleanup();
|
||||
SetupUtils::copydir(APPROOT."/data/$sSourceEnv-modules", APPROOT."/data/$sEnv-modules");
|
||||
|
||||
$this->DeclareExtensionAsRemoved($aExtensionCodesToRemove);
|
||||
|
||||
$oDryRemovalConfig = clone(MetaModel::GetConfig());
|
||||
$oDryRemovalConfig->ChangeModulesPath($sSourceEnv, $this->sFinalEnv);
|
||||
$this->WriteConfigFileSafe($oDryRemovalConfig);
|
||||
|
||||
$sSourceDir = $oDryRemovalConfig->Get('source_dir');
|
||||
$aSearchDirs = $this->GetExtraDirsToCompile($sSourceDir);
|
||||
|
||||
$aModulesToLoad = $this->GetModulesToLoad($sSourceEnv, $aSearchDirs);
|
||||
|
||||
try {
|
||||
ModuleDiscovery::GetModulesOrderedByDependencies($aSearchDirs, true, $aModulesToLoad);
|
||||
} catch (\MissingDependencyException $e) {
|
||||
\IssueLog::Error("Cannot prepare setup due to dependency issue", null, ['msg' => $e->getMessage(), 'modules_to_load' => $aModulesToLoad]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function DeclareExtensionAsRemoved(array $aExtensionCodes): void
|
||||
{
|
||||
$oExtensionsMap = new iTopExtensionsMap($this->sFinalEnv);
|
||||
$oExtensionsMap->DeclareExtensionAsRemoved($aExtensionCodes);
|
||||
}
|
||||
|
||||
private function GetModulesToLoad(string $sSourceEnv, $aSearchDirs): array
|
||||
{
|
||||
$oSourceConfig = new Config(utils::GetConfigFilePath($sSourceEnv));
|
||||
$aChoices = iTopExtensionsMap::GetChoicesFromDatabase($oSourceConfig);
|
||||
$sSourceDir = $oSourceConfig->Get('source_dir');
|
||||
|
||||
$sInstallFilePath = APPROOT.$sSourceDir.'/installation.xml';
|
||||
if (! is_file($sInstallFilePath)) {
|
||||
$sInstallFilePath = null;
|
||||
}
|
||||
|
||||
$aModuleIdsToLoad = InstallationChoicesToModuleConverter::GetInstance()->GetModules($aChoices, $aSearchDirs, $sInstallFilePath);
|
||||
$aModulesToLoad = [];
|
||||
foreach ($aModuleIdsToLoad as $sModuleId) {
|
||||
$oModule = new Module($sModuleId);
|
||||
$sModuleName = $oModule->GetModuleName();
|
||||
$aModulesToLoad[] = $sModuleName;
|
||||
}
|
||||
return $aModulesToLoad;
|
||||
}
|
||||
|
||||
public function Cleanup()
|
||||
{
|
||||
$sEnv = $this->sFinalEnv;
|
||||
SetupUtils::rrmdir(APPROOT."/data/$sEnv-modules");
|
||||
SetupUtils::rrmdir(APPROOT."/data/cache-$sEnv");
|
||||
SetupUtils::rrmdir(APPROOT."/env-$sEnv");
|
||||
SetupUtils::rrmdir(APPROOT."/conf/$sEnv");
|
||||
@unlink(APPROOT."/data/datamodel-$sEnv.xml");
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Setup\FeatureRemoval;
|
||||
|
||||
use MetaModel;
|
||||
|
||||
require_once __DIR__.'/AbstractSetupAudit.php';
|
||||
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
|
||||
|
||||
class InplaceSetupAudit extends AbstractSetupAudit
|
||||
{
|
||||
//file used when present to trigger audit exception when testing specific setups
|
||||
public const GETISSUE_ERROR_MSG_FILE_FORTESTONLY = '.setup_audit_error_msg.txt';
|
||||
|
||||
private string $sEnvAfter;
|
||||
|
||||
public function __construct(array $aClassesBefore, string $sEnvAfter)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->aClassesBefore = $aClassesBefore;
|
||||
$this->sEnvAfter = $sEnvAfter;
|
||||
}
|
||||
|
||||
public function ComputeClasses(): void
|
||||
{
|
||||
if ($this->bClassesInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sCurrentEnvt = MetaModel::GetEnvironment();
|
||||
|
||||
if ($sCurrentEnvt === $this->sEnvAfter) {
|
||||
$this->aClassesAfter = MetaModel::GetClasses();
|
||||
} else {
|
||||
$this->aClassesAfter = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvAfter);
|
||||
}
|
||||
|
||||
$this->bClassesInitialized = true;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Setup\FeatureRemoval;
|
||||
|
||||
use ContextTag;
|
||||
use CoreException;
|
||||
use Exception;
|
||||
use IssueLog;
|
||||
use SetupLog;
|
||||
use utils;
|
||||
|
||||
class ModelReflectionSerializer
|
||||
{
|
||||
private static ModelReflectionSerializer $oInstance;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
final public static function GetInstance(): ModelReflectionSerializer
|
||||
{
|
||||
if (!isset(self::$oInstance)) {
|
||||
self::$oInstance = new ModelReflectionSerializer();
|
||||
}
|
||||
|
||||
return self::$oInstance;
|
||||
}
|
||||
|
||||
final public static function SetInstance(?ModelReflectionSerializer $oInstance): void
|
||||
{
|
||||
self::$oInstance = $oInstance;
|
||||
}
|
||||
|
||||
public function GetModelFromEnvironment(string $sEnv): array
|
||||
{
|
||||
IssueLog::Info(__METHOD__, null, ['env' => $sEnv]);
|
||||
$sPHPExec = trim(utils::GetConfig()->Get('php_path'));
|
||||
$sOutput = "";
|
||||
$iRes = 0;
|
||||
|
||||
$sCommandLine = sprintf("$sPHPExec %s/get_model_reflection.php --env=%s", __DIR__, $sEnv);
|
||||
exec($sCommandLine, $sOutput, $iRes);
|
||||
if ($iRes != 0) {
|
||||
$this->LogErrorWithProperLogger("Cannot get classes", null, ['env' => $sEnv, 'code' => $iRes, "output" => $sOutput]);
|
||||
throw new CoreException("Cannot get classes from env ".$sEnv);
|
||||
}
|
||||
|
||||
$aClasses = json_decode($sOutput[0] ?? null, true);
|
||||
if (false === $aClasses) {
|
||||
$this->LogErrorWithProperLogger("Invalid JSON", null, ['env' => $sEnv, "output" => $sOutput]);
|
||||
throw new Exception("cannot get classes");
|
||||
}
|
||||
|
||||
if (!is_array($aClasses)) {
|
||||
$this->LogErrorWithProperLogger("not an array", null, ['env' => $sEnv, "classes" => $aClasses, "output" => $sOutput]);
|
||||
throw new Exception("cannot get classes from $sEnv");
|
||||
}
|
||||
|
||||
return $aClasses;
|
||||
}
|
||||
|
||||
//could be shared with others in log APIs ?
|
||||
private function LogErrorWithProperLogger($sMessage, $sChannel = null, $aContext = []): void
|
||||
{
|
||||
if (ContextTag::Check(ContextTag::TAG_SETUP)) {
|
||||
SetupLog::Error($sMessage, $sChannel, $aContext);
|
||||
} else {
|
||||
IssueLog::Error($sMessage, $sChannel, $aContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Setup\FeatureRemoval;
|
||||
|
||||
require_once __DIR__.'/AbstractSetupAudit.php';
|
||||
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
|
||||
|
||||
class SetupAudit extends AbstractSetupAudit
|
||||
{
|
||||
//file used when present to trigger audit exception when testing specific setups
|
||||
public const GETISSUE_ERROR_MSG_FILE_FORTESTONLY = '.setup_audit_error_msg.txt';
|
||||
|
||||
private string $sEnvBefore;
|
||||
private string $sEnvAfter;
|
||||
|
||||
public function __construct(string $sEnvBefore, string $sEnvAfter)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->sEnvBefore = $sEnvBefore;
|
||||
$this->sEnvAfter = $sEnvAfter;
|
||||
}
|
||||
|
||||
public function ComputeClasses(): void
|
||||
{
|
||||
if ($this->bClassesInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->aClassesBefore = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvBefore);
|
||||
$this->aClassesAfter = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvAfter);
|
||||
|
||||
$this->bClassesInitialized = true;
|
||||
}
|
||||
|
||||
public function GetRemovedClasses(): array
|
||||
{
|
||||
$this->ComputeClasses();
|
||||
|
||||
if (count($this->aRemovedClasses) == 0) {
|
||||
if (count($this->aClassesBefore) == 0) {
|
||||
return $this->aRemovedClasses;
|
||||
}
|
||||
|
||||
if (count($this->aClassesAfter) == 0) {
|
||||
return $this->aRemovedClasses;
|
||||
}
|
||||
|
||||
$aExtensionsNames = array_diff($this->aClassesBefore, $this->aClassesAfter);
|
||||
$this->aRemovedClasses = [];
|
||||
$aClasses = array_values($aExtensionsNames);
|
||||
sort($aClasses);
|
||||
|
||||
foreach ($aClasses as $i => $sClass) {
|
||||
$this->aRemovedClasses[] = $sClass;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->aRemovedClasses;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once(dirname(__DIR__, 2).'/approot.inc.php');
|
||||
require_once(APPROOT.'application/application.inc.php');
|
||||
$sEnv = null;
|
||||
if (isset($argv)) {
|
||||
foreach ($argv as $iArg => $sArg) {
|
||||
if (preg_match('/^--env=(.*)$/', $sArg, $aMatches)) {
|
||||
$sEnv = $aMatches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($sEnv)) {
|
||||
echo "No environment provided (--env) to read datamodel.";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$sConfFile = utils::GetConfigFilePath($sEnv);
|
||||
|
||||
try {
|
||||
MetaModel::Startup($sConfFile, false /* $bModelOnly */, false /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
|
||||
} catch (\Throwable $e) {
|
||||
echo $e->getMessage();
|
||||
echo $e->getTraceAsString();
|
||||
\SetupLog::Error(
|
||||
"Cannot read model from provided environment",
|
||||
null,
|
||||
[
|
||||
'env' => $sEnv,
|
||||
'error' => $e->getMessage(),
|
||||
'stack' => $e->getTraceAsString(),
|
||||
]
|
||||
);
|
||||
echo "Cannot read model from provided environment";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$aClasses = MetaModel::GetClasses();
|
||||
|
||||
echo json_encode($aClasses);
|
||||
@@ -1,144 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
|
||||
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
|
||||
|
||||
require_once(APPROOT.'/setup/parameters.class.inc.php');
|
||||
require_once(APPROOT.'/core/cmdbsource.class.inc.php');
|
||||
require_once(APPROOT.'/setup/modulediscovery.class.inc.php');
|
||||
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
|
||||
|
||||
/**
|
||||
* Basic helper class to describe an extension, with some characteristics and a list of modules
|
||||
*/
|
||||
class iTopExtension
|
||||
{
|
||||
public const SOURCE_WIZARD = 'datamodels';
|
||||
public const SOURCE_MANUAL = 'extensions';
|
||||
public const SOURCE_REMOTE = 'data';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sCode;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sVersion;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sInstalledVersion;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sLabel;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sDescription;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sSource;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $bMandatory;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sMoreInfoUrl;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $bMarkedAsChosen;
|
||||
/**
|
||||
* If null, check if at least one module cannot be uninstalled
|
||||
* @var bool|null
|
||||
*/
|
||||
public ?bool $bCanBeUninstalled = null;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $bVisible;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public $aModules;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public $aModuleVersion;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public $aModuleInfo;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sSourceDir;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $aMissingDependencies;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public bool $bInstalled = false;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public bool $bRemovedFromDisk = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->sCode = '';
|
||||
$this->sLabel = '';
|
||||
$this->sDescription = '';
|
||||
$this->sSource = self::SOURCE_WIZARD;
|
||||
$this->bMandatory = false;
|
||||
$this->sMoreInfoUrl = '';
|
||||
$this->bMarkedAsChosen = false;
|
||||
$this->sVersion = ITOP_VERSION;
|
||||
$this->sInstalledVersion = '';
|
||||
$this->aModules = [];
|
||||
$this->aModuleVersion = [];
|
||||
$this->aModuleInfo = [];
|
||||
$this->sSourceDir = '';
|
||||
$this->bVisible = true;
|
||||
$this->aMissingDependencies = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.3.0
|
||||
* @return bool
|
||||
*/
|
||||
public function CanBeUninstalled(): bool
|
||||
{
|
||||
if (!is_null($this->bCanBeUninstalled)) {
|
||||
return $this->bCanBeUninstalled;
|
||||
}
|
||||
foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo) {
|
||||
if ($aModuleInfo['uninstallable'] !== 'yes') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1801,7 +1801,7 @@ EOF
|
||||
*/
|
||||
public function FindModules()
|
||||
{
|
||||
$aAvailableModules = ModuleDiscovery::GetModulesOrderedByDependencies($this->aRootDirs);
|
||||
$aAvailableModules = ModuleDiscovery::GetAvailableModules($this->aRootDirs);
|
||||
$aResult = [];
|
||||
foreach ($aAvailableModules as $sId => $aModule) {
|
||||
$oModule = new MFModule($sId, $aModule['root_dir'], $aModule['label'], isset($aModule['auto_select']));
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Setup\ModuleDependency;
|
||||
|
||||
require_once(APPROOT.'/setup/runtimeenv.class.inc.php');
|
||||
|
||||
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
|
||||
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
|
||||
use RunTimeEnvironment;
|
||||
|
||||
/**
|
||||
* Class that handles a module dependency
|
||||
* Dependency expression example : (moduleA/123 || moduleB>456)
|
||||
*/
|
||||
class DependencyExpression
|
||||
{
|
||||
private static PhpExpressionEvaluator $oPhpExpressionEvaluator;
|
||||
|
||||
private string $sDependencyExpression;
|
||||
private bool $bValid = true;
|
||||
private bool $bResolved = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool> $aRemainingModuleNamesToResolve
|
||||
*/
|
||||
private array $aRemainingModuleNamesToResolve;
|
||||
|
||||
/**
|
||||
* @var array<string, array> $aParamsPerModuleId
|
||||
*/
|
||||
private array $aParamsPerModuleId;
|
||||
|
||||
public function __construct(string $sDependencyExpression)
|
||||
{
|
||||
$this->sDependencyExpression = $sDependencyExpression;
|
||||
$this->aParamsPerModuleId = [];
|
||||
$this->aRemainingModuleNamesToResolve = [];
|
||||
|
||||
if (preg_match_all('/([^\(\)&| ]+)/', $sDependencyExpression, $aMatches)) {
|
||||
foreach ($aMatches as $aMatch) {
|
||||
foreach ($aMatch as $sModuleId) {
|
||||
if (!array_key_exists($sModuleId, $this->aParamsPerModuleId)) {
|
||||
// $sModuleId in the dependency string is made of a <name>/<optional_operator><version>
|
||||
// where the operator is < <= = > >= (by default >=)
|
||||
$aModuleMatches = [];
|
||||
if (preg_match('|^([^/]+)/(<?>?=?)([^><=]+)$|', $sModuleId, $aModuleMatches)) {
|
||||
$sModuleName = $aModuleMatches[1];
|
||||
$this->aRemainingModuleNamesToResolve[$sModuleName] = true;
|
||||
$sOperator = $aModuleMatches[2];
|
||||
if ($sOperator == '') {
|
||||
$sOperator = '>=';
|
||||
}
|
||||
$sExpectedVersion = $aModuleMatches[3];
|
||||
$this->aParamsPerModuleId[$sModuleId] = [$sModuleName, $sOperator, $sExpectedVersion];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->bValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
|
||||
{
|
||||
if (!isset(self::$oPhpExpressionEvaluator)) {
|
||||
self::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
|
||||
}
|
||||
|
||||
return self::$oPhpExpressionEvaluator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return module names potentially required by current dependency
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function GetRemainingModuleNamesToResolve(): array
|
||||
{
|
||||
return array_keys($this->aRemainingModuleNamesToResolve);
|
||||
}
|
||||
|
||||
public function IsResolved(): bool
|
||||
{
|
||||
return $this->bResolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if dependency is resolved with current list of module versions
|
||||
*
|
||||
* @param array $aResolvedModuleVersions : versions by module names dict
|
||||
* @param array $aAllModuleNames : modules names dict
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function UpdateModuleResolutionState(array $aResolvedModuleVersions, array $aAllModuleNames): void
|
||||
{
|
||||
if (!$this->bValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
$aReplacements = [];
|
||||
|
||||
$bDelayEvaluation = false;
|
||||
foreach ($this->aParamsPerModuleId as $sModuleId => list($sModuleName, $sOperator, $sExpectedVersion)) {
|
||||
if (array_key_exists($sModuleName, $aResolvedModuleVersions)) {
|
||||
// module is resolved, check the version
|
||||
$sCurrentVersion = $aResolvedModuleVersions[$sModuleName];
|
||||
if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) {
|
||||
if (array_key_exists($sModuleName, $this->aRemainingModuleNamesToResolve)) {
|
||||
unset($this->aRemainingModuleNamesToResolve[$sModuleName]);
|
||||
}
|
||||
$aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing
|
||||
// a function call that results in a runtime fatal error
|
||||
} else {
|
||||
$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
|
||||
// a function call that results in a runtime fatal error
|
||||
}
|
||||
} else {
|
||||
// module is not resolved yet
|
||||
|
||||
if (array_key_exists($sModuleName, $aAllModuleNames)) {
|
||||
//Weird piece of code that covers below usecase:
|
||||
//module B dependency: 'moduleA || true'
|
||||
// if moduleA not present on disk, whole expression can be evaluated and may be resolved
|
||||
// if moduleA present on disk, we need to sort moduleB after moduleA. expression cannot be resolved yet
|
||||
$bDelayEvaluation = true;
|
||||
} else {
|
||||
$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if ($bDelayEvaluation) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bResult = false;
|
||||
$sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $this->sDependencyExpression);
|
||||
try {
|
||||
$bResult = self::GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($sBooleanExpr);
|
||||
} catch (ModuleFileReaderException $e) {
|
||||
//logged already
|
||||
echo "Failed to parse the boolean Expression = '$sBooleanExpr'<br/>";
|
||||
}
|
||||
|
||||
$this->bResolved = $bResult;
|
||||
}
|
||||
|
||||
public function IsValid(): bool
|
||||
{
|
||||
return $this->bValid;
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Setup\ModuleDependency;
|
||||
|
||||
require_once(__DIR__.'/dependencyexpression.class.inc.php');
|
||||
use ModuleDiscovery;
|
||||
|
||||
/**
|
||||
* Class that handles a modules and all its dependencies
|
||||
*/
|
||||
class Module
|
||||
{
|
||||
private string $sModuleId;
|
||||
private string $sModuleName;
|
||||
private string $sVersion;
|
||||
|
||||
/**
|
||||
* @var array<string> $aInitialDependencyExpressions
|
||||
*/
|
||||
private array $aInitialDependencyExpressions;
|
||||
|
||||
/**
|
||||
* @var array<string, DependencyExpression> $aRemainingDependenciesToResolve
|
||||
*/
|
||||
public array $aRemainingDependenciesToResolve;
|
||||
|
||||
public function __construct(string $sModuleId)
|
||||
{
|
||||
$this->sModuleId = $sModuleId;
|
||||
list($this->sModuleName, $this->sVersion) = ModuleDiscovery::GetModuleName($sModuleId);
|
||||
}
|
||||
|
||||
public function IsDependencyExpressionResolved(string $sDependencyExpression): bool
|
||||
{
|
||||
return ! array_key_exists($sDependencyExpression, $this->aRemainingDependenciesToResolve);
|
||||
}
|
||||
|
||||
public function GetDependencyResolutionFeedback(): array
|
||||
{
|
||||
$aDepsWithIcons = [];
|
||||
|
||||
foreach ($this->aInitialDependencyExpressions as $sDependencyExpression) {
|
||||
if (! $this->IsDependencyExpressionResolved($sDependencyExpression)) {
|
||||
$aDepsWithIcons[] = '❌ '.$sDependencyExpression;
|
||||
}
|
||||
}
|
||||
return $aDepsWithIcons;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function GetModuleName()
|
||||
{
|
||||
return $this->sModuleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function GetVersion()
|
||||
{
|
||||
return $this->sVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function GetModuleId()
|
||||
{
|
||||
return $this->sModuleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aAllDependencyExpressions: list of dependencies (string)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function SetDependencies(array $aAllDependencyExpressions): void
|
||||
{
|
||||
$this->aInitialDependencyExpressions = $aAllDependencyExpressions;
|
||||
$this->aRemainingDependenciesToResolve = [];
|
||||
|
||||
foreach ($aAllDependencyExpressions as $sDependencyExpression) {
|
||||
$this->aRemainingDependenciesToResolve[$sDependencyExpression] = new DependencyExpression($sDependencyExpression);
|
||||
}
|
||||
}
|
||||
|
||||
public function IsResolved(): bool
|
||||
{
|
||||
return (0 === count($this->aRemainingDependenciesToResolve));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if module dependencies are resolved with current list of module versions
|
||||
* @param array<string, string> $aResolvedModuleVersions : versions by module names dict
|
||||
* @param array<string> $aAllModuleNames : resolved modules names
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function UpdateModuleResolutionState(array $aResolvedModuleVersions, array $aAllModuleNames): void
|
||||
{
|
||||
$aNextDependencies = [];
|
||||
|
||||
foreach ($this->aRemainingDependenciesToResolve as $sDependencyExpression => $oModuleDependency) {
|
||||
/** @var DependencyExpression $oModuleDependency*/
|
||||
$oModuleDependency->UpdateModuleResolutionState($aResolvedModuleVersions, $aAllModuleNames);
|
||||
if (!$oModuleDependency->IsResolved()) {
|
||||
$aNextDependencies[$sDependencyExpression] = $oModuleDependency;
|
||||
}
|
||||
}
|
||||
|
||||
$this->aRemainingDependenciesToResolve = $aNextDependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string> list of unique module names
|
||||
*/
|
||||
public function GetUnresolvedDependencyModuleNames(): array
|
||||
{
|
||||
$aRes = [];
|
||||
foreach ($this->aRemainingDependenciesToResolve as $sDependencyExpression => $oModuleDependency) {
|
||||
/** @var DependencyExpression $oModuleDependency */
|
||||
$aRes = array_merge($aRes, $oModuleDependency->GetRemainingModuleNamesToResolve());
|
||||
}
|
||||
|
||||
return array_unique($aRes);
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Setup\ModuleDependency;
|
||||
|
||||
require_once(__DIR__.'/module.class.inc.php');
|
||||
|
||||
use MissingDependencyException;
|
||||
|
||||
/**
|
||||
* Class that sorts module dependencies
|
||||
*/
|
||||
class ModuleDependencySort
|
||||
{
|
||||
private static ModuleDependencySort $oInstance;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
final public static function GetInstance(): ModuleDependencySort
|
||||
{
|
||||
if (!isset(self::$oInstance)) {
|
||||
self::$oInstance = new ModuleDependencySort();
|
||||
}
|
||||
|
||||
return self::$oInstance;
|
||||
}
|
||||
|
||||
final public static function SetInstance(?ModuleDependencySort $oInstance): void
|
||||
{
|
||||
self::$oInstance = $oInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort a list of modules, based on their (inter) dependencies
|
||||
*
|
||||
* @param array $aModules The list of modules to process: 'id' => $aModuleInfo
|
||||
* @param bool $bAbortOnMissingDependency ...
|
||||
*
|
||||
* @return array
|
||||
* @throws \MissingDependencyException
|
||||
*/
|
||||
public function GetModulesOrderedForInstallation($aModules, $bAbortOnMissingDependency = false)
|
||||
{
|
||||
// Filter modules to compute
|
||||
$aUnresolvedDependencyModules = [];
|
||||
$aAllModuleNames = [];
|
||||
foreach ($aModules as $sModuleId => $aModule) {
|
||||
$oModule = new Module($sModuleId);
|
||||
$sModuleName = $oModule->GetModuleName();
|
||||
$oModule->SetDependencies($aModule['dependencies']);
|
||||
$aUnresolvedDependencyModules[$sModuleId] = $oModule;
|
||||
$aAllModuleNames[$sModuleName] = true;
|
||||
}
|
||||
|
||||
// Make sure order is deterministic (alphabtical order)
|
||||
ksort($aUnresolvedDependencyModules);
|
||||
|
||||
//Attempt to resolve module dependencies
|
||||
$aOrderedModules = [];
|
||||
$aResolvedModuleVersions = [];
|
||||
$iPreviousUnresolvedCount = -1;
|
||||
//loop until no dependency is resolved
|
||||
while ($iPreviousUnresolvedCount !== count($aUnresolvedDependencyModules)) {
|
||||
$iPreviousUnresolvedCount = count($aUnresolvedDependencyModules);
|
||||
if ($iPreviousUnresolvedCount === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) {
|
||||
/** @var Module $oModule */
|
||||
$oModule->UpdateModuleResolutionState($aResolvedModuleVersions, $aAllModuleNames);
|
||||
if ($oModule->IsResolved()) {
|
||||
$aOrderedModules[] = $sModuleId;
|
||||
$aResolvedModuleVersions[$oModule->GetModuleName()] = $oModule->GetVersion();
|
||||
unset($aUnresolvedDependencyModules[$sModuleId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Report unresolved dependencies
|
||||
if ($bAbortOnMissingDependency && count($aUnresolvedDependencyModules) > 0) {
|
||||
$this->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
|
||||
|
||||
$aUnresolvedModulesInfo = [];
|
||||
$aModuleDeps = [];
|
||||
foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) {
|
||||
$aModule = $aModules[$sModuleId];
|
||||
$aDepsWithIcons = $oModule->GetDependencyResolutionFeedback();
|
||||
|
||||
$aModuleDeps[] = "{$aModule['label']} (id: $sModuleId) depends on: ".implode(' + ', $aDepsWithIcons);
|
||||
$aUnresolvedModulesInfo[$sModuleId] = ['module' => $aModule, 'dependencies' => $aDepsWithIcons];
|
||||
}
|
||||
$sMessage = "The following modules have unmet dependencies:\n".implode(",\n", $aModuleDeps);
|
||||
$oException = new MissingDependencyException($sMessage);
|
||||
$oException->aModulesInfo = $aUnresolvedModulesInfo;
|
||||
throw $oException;
|
||||
}
|
||||
|
||||
// Return the ordered list, so that the dependencies are met...
|
||||
$aResult = [];
|
||||
foreach ($aOrderedModules as $sId) {
|
||||
$aResult[$sId] = $aModules[$sId];
|
||||
}
|
||||
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is key as it sorts modules by their dependencies (topological sort).
|
||||
* Modules with less dependencies are first.
|
||||
* When module A depends from module B with same amount of dependencies, moduleB is first.
|
||||
* This order can deal with
|
||||
* - cyclic dependencies
|
||||
* - further versions of same module (name)
|
||||
*
|
||||
* @param array $aUnresolvedDependencyModules : dict of Module objects by moduleId key
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function SortModulesByCountOfDepencenciesDescending(array &$aUnresolvedDependencyModules): void
|
||||
{
|
||||
$aCountDepsByModuleId = [];
|
||||
$aDependsOnModuleName = [];
|
||||
|
||||
foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) {
|
||||
/** @var Module $oModule */
|
||||
$aDependsOnModuleName[$oModule->GetModuleName()] = [];
|
||||
}
|
||||
|
||||
foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) {
|
||||
$iInDegreeCounter = 0;
|
||||
/** @var Module $oModule */
|
||||
$aUnresolvedDependencyModuleNames = $oModule->GetUnresolvedDependencyModuleNames();
|
||||
foreach ($aUnresolvedDependencyModuleNames as $sModuleName) {
|
||||
if (array_key_exists($sModuleName, $aDependsOnModuleName)) {
|
||||
$aDependsOnModuleName[$sModuleName][] = $sModuleId;
|
||||
$iInDegreeCounter++;
|
||||
}
|
||||
}
|
||||
//include all modules
|
||||
$iInDegreeCounterIncludingOutsideModules = count($oModule->GetUnresolvedDependencyModuleNames());
|
||||
$aCountDepsByModuleId[$sModuleId] = [$iInDegreeCounter, $iInDegreeCounterIncludingOutsideModules, $sModuleId];
|
||||
}
|
||||
|
||||
$aRes = [];
|
||||
while (count($aUnresolvedDependencyModules) > 0) {
|
||||
asort($aCountDepsByModuleId);
|
||||
|
||||
uasort($aCountDepsByModuleId, function (array $aDeps1, array $aDeps2) {
|
||||
//compare $iInDegreeCounter
|
||||
$res = $aDeps1[0] - $aDeps2[0];
|
||||
if ($res != 0) {
|
||||
return $res;
|
||||
}
|
||||
|
||||
//compare $iInDegreeCounterIncludingOutsideModules
|
||||
$res = $aDeps1[1] - $aDeps2[1];
|
||||
if ($res != 0) {
|
||||
return $res;
|
||||
}
|
||||
|
||||
//alphabetical order at least
|
||||
return strcmp($aDeps1[2], $aDeps2[2]);
|
||||
});
|
||||
|
||||
$bOneLoopAtLeast = false;
|
||||
foreach ($aCountDepsByModuleId as $sModuleId => $iInDegreeCounter) {
|
||||
$oModule = $aUnresolvedDependencyModules[$sModuleId];
|
||||
|
||||
if ($bOneLoopAtLeast && ($iInDegreeCounter > 0)) {
|
||||
break;
|
||||
}
|
||||
|
||||
unset($aUnresolvedDependencyModules[$sModuleId]);
|
||||
unset($aCountDepsByModuleId[$sModuleId]);
|
||||
|
||||
$aRes[$sModuleId] = $oModule;
|
||||
|
||||
//when 2 versions of the same module (name) below array has been removed already
|
||||
if (array_key_exists($oModule->GetModuleName(), $aDependsOnModuleName)) {
|
||||
foreach ($aDependsOnModuleName[$oModule->GetModuleName()] as $sModuleId2) {
|
||||
if (!array_key_exists($sModuleId2, $aCountDepsByModuleId)) {
|
||||
continue;
|
||||
}
|
||||
$aDepCount = $aCountDepsByModuleId[$sModuleId2];
|
||||
$iInDegreeCounter = $aDepCount[0] - 1;
|
||||
$iInDegreeCounterIncludingOutsideModules = $aDepCount[1];
|
||||
$aCountDepsByModuleId[$sModuleId2] = [$iInDegreeCounter, $iInDegreeCounterIncludingOutsideModules, $sModuleId2];
|
||||
}
|
||||
|
||||
unset($aDependsOnModuleName[$oModule->GetModuleName()]);
|
||||
}
|
||||
|
||||
$bOneLoopAtLeast = true;
|
||||
}
|
||||
}
|
||||
|
||||
$aUnresolvedDependencyModules = $aRes;
|
||||
}
|
||||
}
|
||||
319
setup/modulediscovery.class.inc.php
Executable file → Normal file
319
setup/modulediscovery.class.inc.php
Executable file → Normal file
@@ -21,14 +21,10 @@
|
||||
*/
|
||||
|
||||
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
|
||||
use Combodo\iTop\Setup\ModuleDependency\Module;
|
||||
use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort;
|
||||
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
|
||||
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
|
||||
|
||||
require_once(APPROOT.'setup/modulediscovery/ModuleFileReader.php');
|
||||
require_once(__DIR__.'/moduledependency/moduledependencysort.class.inc.php');
|
||||
require_once(__DIR__.'/itopextension.class.inc.php');
|
||||
|
||||
class MissingDependencyException extends CoreException
|
||||
{
|
||||
@@ -95,9 +91,6 @@ class ModuleDiscovery
|
||||
protected static $m_aModules = [];
|
||||
protected static $m_aModuleVersionByName = [];
|
||||
|
||||
/** @var array<\iTopExtension> $m_aRemovedExtensions */
|
||||
protected static array $m_aRemovedExtensions = [];
|
||||
|
||||
// All the entries below are list of file paths relative to the module directory
|
||||
protected static $m_aFilesList = ['datamodel', 'webservice', 'dictionary', 'data.struct', 'data.sample'];
|
||||
|
||||
@@ -123,6 +116,10 @@ class ModuleDiscovery
|
||||
if (is_null($aArgs) || ! is_array($aArgs)) {
|
||||
throw new ModuleFileReaderException("Error parsing module file args", 0, null, $sFilePath);
|
||||
}
|
||||
if (!array_key_exists('itop_version', $aArgs)) {
|
||||
// Assume 1.0.2
|
||||
$aArgs['itop_version'] = '1.0.2';
|
||||
}
|
||||
foreach (array_keys(self::$m_aModuleArgs) as $sArgName) {
|
||||
if (!array_key_exists($sArgName, $aArgs)) {
|
||||
throw new Exception("Module '$sId': missing argument '$sArgName'");
|
||||
@@ -134,10 +131,6 @@ class ModuleDiscovery
|
||||
|
||||
list($sModuleName, $sModuleVersion) = static::GetModuleName($sId);
|
||||
|
||||
if (self::IsModuleInExtensionList(self::$m_aRemovedExtensions, $sModuleName, $sModuleVersion, $aArgs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (array_key_exists($sModuleName, self::$m_aModuleVersionByName)) {
|
||||
if (version_compare($sModuleVersion, self::$m_aModuleVersionByName[$sModuleName]['version'], '>')) {
|
||||
// Newer version, let's upgrade
|
||||
@@ -196,6 +189,21 @@ class ModuleDiscovery
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of "discovered" modules, ordered based on their (inter) dependencies
|
||||
*
|
||||
* @param bool $bAbortOnMissingDependency ...
|
||||
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
|
||||
*
|
||||
* @return array
|
||||
* @throws \MissingDependencyException
|
||||
*/
|
||||
protected static function GetModules($bAbortOnMissingDependency = false, $aModulesToLoad = null)
|
||||
{
|
||||
// Order the modules to take into account their inter-dependencies
|
||||
return self::OrderModulesByDependencies(self::$m_aModules, $bAbortOnMissingDependency, $aModulesToLoad);
|
||||
}
|
||||
|
||||
/**
|
||||
* Arrange an list of modules, based on their (inter) dependencies
|
||||
* @param array $aModules The list of modules to process: 'id' => $aModuleInfo
|
||||
@@ -203,43 +211,166 @@ class ModuleDiscovery
|
||||
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
|
||||
* @return array
|
||||
* @throws \MissingDependencyException
|
||||
*/
|
||||
*/
|
||||
public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
|
||||
{
|
||||
if (is_null($aModulesToLoad) && count(self::$m_aRemovedExtensions) === 0) {
|
||||
$aFilteredModules = $aModules;
|
||||
} else {
|
||||
$aFilteredModules = [];
|
||||
foreach ($aModules as $sModuleId => $aModuleInfo) {
|
||||
$oModule = new Module($sModuleId);
|
||||
$sModuleName = $oModule->GetModuleName();
|
||||
|
||||
if (self::IsModuleInExtensionList(self::$m_aRemovedExtensions, $sModuleName, $oModule->GetVersion(), $aModuleInfo)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad)) {
|
||||
$aFilteredModules[$sModuleId] = $aModuleInfo;
|
||||
}
|
||||
// Order the modules to take into account their inter-dependencies
|
||||
$aDependencies = [];
|
||||
$aSelectedModules = [];
|
||||
foreach ($aModules as $sId => $aModule) {
|
||||
list($sModuleName, ) = self::GetModuleName($sId);
|
||||
if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad)) {
|
||||
$aDependencies[$sId] = $aModule['dependencies'];
|
||||
$aSelectedModules[$sModuleName] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aFilteredModules, $bAbortOnMissingDependency);
|
||||
ksort($aDependencies);
|
||||
$aOrderedModules = [];
|
||||
$iLoopCount = 0;
|
||||
while (($iLoopCount < count($aModules)) && (count($aDependencies) > 0)) {
|
||||
foreach ($aDependencies as $sId => $aRemainingDeps) {
|
||||
$bDependenciesSolved = true;
|
||||
foreach ($aRemainingDeps as $sDepId) {
|
||||
if (!self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules)) {
|
||||
$bDependenciesSolved = false;
|
||||
}
|
||||
}
|
||||
if ($bDependenciesSolved) {
|
||||
$aOrderedModules[] = $sId;
|
||||
unset($aDependencies[$sId]);
|
||||
}
|
||||
}
|
||||
$iLoopCount++;
|
||||
}
|
||||
if ($bAbortOnMissingDependency && count($aDependencies) > 0) {
|
||||
$aModulesInfo = [];
|
||||
$aModuleDeps = [];
|
||||
foreach ($aDependencies as $sId => $aDeps) {
|
||||
$aModule = $aModules[$sId];
|
||||
$aDepsWithIcons = [];
|
||||
foreach ($aDeps as $sIndex => $sDepId) {
|
||||
if (self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules)) {
|
||||
$aDepsWithIcons[$sIndex] = '✅ '.$sDepId;
|
||||
} else {
|
||||
$aDepsWithIcons[$sIndex] = '❌ '.$sDepId;
|
||||
}
|
||||
}
|
||||
$aModuleDeps[] = "{$aModule['label']} (id: $sId) depends on: ".implode(' + ', $aDepsWithIcons);
|
||||
$aModulesInfo[$sId] = ['module' => $aModule, 'dependencies' => $aDepsWithIcons];
|
||||
}
|
||||
$sMessage = "The following modules have unmet dependencies:\n".implode(",\n", $aModuleDeps);
|
||||
$oException = new MissingDependencyException($sMessage);
|
||||
$oException->aModulesInfo = $aModulesInfo;
|
||||
throw $oException;
|
||||
}
|
||||
// Return the ordered list, so that the dependencies are met...
|
||||
$aResult = [];
|
||||
foreach ($aOrderedModules as $sId) {
|
||||
$aResult[$sId] = $aModules[$sId];
|
||||
}
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<\iTopExtension> $aRemovedExtension
|
||||
* @return void
|
||||
* Remove the duplicate modules (i.e. modules with the same name but with a different version) from the supplied list of modules
|
||||
* @param array $aModules
|
||||
* @return array The ordered modules as a duplicate-free list of modules
|
||||
*/
|
||||
public static function DeclareRemovedExtensions(array $aRemovedExtension): void
|
||||
public static function RemoveDuplicateModules($aModules)
|
||||
{
|
||||
if (self::$m_aRemovedExtensions != $aRemovedExtension) {
|
||||
self::ResetCache();
|
||||
}
|
||||
self::$m_aRemovedExtensions = $aRemovedExtension;
|
||||
// No longer needed, kept only for compatibility
|
||||
// The de-duplication is now done directly by the AddModule method
|
||||
return $aModules;
|
||||
}
|
||||
|
||||
private static function Init($aSearchDirs): void
|
||||
private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
|
||||
{
|
||||
if (!isset(static::$oPhpExpressionEvaluator)) {
|
||||
static::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
|
||||
}
|
||||
|
||||
return static::$oPhpExpressionEvaluator;
|
||||
}
|
||||
|
||||
protected static function DependencyIsResolved($sDepString, $aOrderedModules, $aSelectedModules)
|
||||
{
|
||||
$bResult = false;
|
||||
$aModuleVersions = [];
|
||||
// Separate the module names from their version for an easier comparison later
|
||||
foreach ($aOrderedModules as $sModuleId) {
|
||||
list($sModuleName, $sVersion) = self::GetModuleName($sModuleId);
|
||||
$aModuleVersions[$sModuleName] = $sVersion;
|
||||
}
|
||||
if (preg_match_all('/([^\(\)&| ]+)/', $sDepString, $aMatches)) {
|
||||
$aReplacements = [];
|
||||
$aPotentialPrerequisites = [];
|
||||
foreach ($aMatches as $aMatch) {
|
||||
foreach ($aMatch as $sModuleId) {
|
||||
// $sModuleId in the dependency string is made of a <name>/<optional_operator><version>
|
||||
// where the operator is < <= = > >= (by default >=)
|
||||
$aModuleMatches = [];
|
||||
if (preg_match('|^([^/]+)/(<?>?=?)([^><=]+)$|', $sModuleId, $aModuleMatches)) {
|
||||
$sModuleName = $aModuleMatches[1];
|
||||
$aPotentialPrerequisites[$sModuleName] = true;
|
||||
$sOperator = $aModuleMatches[2];
|
||||
if ($sOperator == '') {
|
||||
$sOperator = '>=';
|
||||
}
|
||||
$sExpectedVersion = $aModuleMatches[3];
|
||||
if (array_key_exists($sModuleName, $aModuleVersions)) {
|
||||
// module is present, check the version
|
||||
$sCurrentVersion = $aModuleVersions[$sModuleName];
|
||||
if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) {
|
||||
$aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing
|
||||
// a function call that results in a runtime fatal error
|
||||
} else {
|
||||
$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
|
||||
// a function call that results in a runtime fatal error
|
||||
}
|
||||
} else {
|
||||
// module is not present
|
||||
$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
|
||||
// a function call that results in a runtime fatal error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$bMissingPrerequisite = false;
|
||||
foreach (array_keys($aPotentialPrerequisites) as $sModuleName) {
|
||||
if (array_key_exists($sModuleName, $aSelectedModules)) {
|
||||
// This module is actually a prerequisite
|
||||
if (!array_key_exists($sModuleName, $aModuleVersions)) {
|
||||
$bMissingPrerequisite = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($bMissingPrerequisite) {
|
||||
$bResult = false;
|
||||
} else {
|
||||
$sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $sDepString);
|
||||
try {
|
||||
$bResult = self::GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($sBooleanExpr);
|
||||
} catch (ModuleFileReaderException $e) {
|
||||
//logged already
|
||||
echo "Failed to parse the boolean Expression = '$sBooleanExpr'<br/>";
|
||||
}
|
||||
}
|
||||
}
|
||||
return $bResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search (on the disk) for all defined iTop modules, load them and returns the list (as an array)
|
||||
* of the possible iTop modules to install
|
||||
*
|
||||
* @param $aSearchDirs array of directories to search (absolute paths)
|
||||
* @param bool $bAbortOnMissingDependency ...
|
||||
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
|
||||
*
|
||||
* @return array A big array moduleID => ModuleData
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function GetAvailableModules($aSearchDirs, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
|
||||
{
|
||||
if (self::$m_aSearchDirs != $aSearchDirs) {
|
||||
self::ResetCache();
|
||||
@@ -258,60 +389,13 @@ class ModuleDiscovery
|
||||
clearstatcache();
|
||||
self::ListModuleFiles(basename($sSearchDir), dirname($sSearchDir));
|
||||
}
|
||||
return self::GetModules($bAbortOnMissingDependency, $aModulesToLoad);
|
||||
} else {
|
||||
// Reuse the previous results
|
||||
return self::GetModules($bAbortOnMissingDependency, $aModulesToLoad);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all modules found on disk ordered by dependencies. Skipping modules coming from extensions declared as removed (@see ModuleDiscovery::DeclareRemovedExtensions)
|
||||
* @param $aSearchDirs array of directories to search (absolute paths)
|
||||
* @param bool $bAbortOnMissingDependency ...
|
||||
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
|
||||
*
|
||||
* @return array A big array moduleID => ModuleData
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function GetModulesOrderedByDependencies($aSearchDirs, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
|
||||
{
|
||||
self::Init($aSearchDirs);
|
||||
|
||||
return self::OrderModulesByDependencies(self::$m_aModules, $bAbortOnMissingDependency, $aModulesToLoad);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use \ModuleDiscovery::GetModulesOrderedByDependencies instead
|
||||
*/
|
||||
public static function GetAvailableModules($aSearchDirs, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
|
||||
{
|
||||
return ModuleDiscovery::GetModulesOrderedByDependencies($aSearchDirs, $bAbortOnMissingDependency, $aModulesToLoad);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all modules found on disk (without any dependency consideration). Skipping modules coming from extensions declared as removed (@see ModuleDiscovery::DeclareRemovedExtensions)
|
||||
*
|
||||
* @param $aSearchDirs array of directories to search (absolute paths)
|
||||
*
|
||||
* @return array A big array moduleID => ModuleData
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function GetAllModules($aSearchDirs)
|
||||
{
|
||||
self::Init($aSearchDirs);
|
||||
|
||||
$aNonRemovedModules = [];
|
||||
foreach (self::$m_aModules as $sModuleId => $aModuleInfo) {
|
||||
$oModule = new Module($sModuleId);
|
||||
$sModuleName = $oModule->GetModuleName();
|
||||
|
||||
if (self::IsModuleInExtensionList(self::$m_aRemovedExtensions, $sModuleName, $oModule->GetVersion(), $aModuleInfo)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$aNonRemovedModules[$sModuleId] = $aModuleInfo;
|
||||
}
|
||||
|
||||
return $aNonRemovedModules;
|
||||
}
|
||||
|
||||
public static function ResetCache()
|
||||
{
|
||||
self::$m_aSearchDirs = null;
|
||||
@@ -321,12 +405,10 @@ class ModuleDiscovery
|
||||
|
||||
/**
|
||||
* Helper function to interpret the name of a module
|
||||
*
|
||||
* @param $sModuleId string Identifier of the module, in the form 'name/version'
|
||||
*
|
||||
* @return array of 2 elements (name, version)
|
||||
* @return array(name, version)
|
||||
*/
|
||||
public static function GetModuleName($sModuleId): array
|
||||
public static function GetModuleName($sModuleId)
|
||||
{
|
||||
$aMatches = [];
|
||||
if (preg_match('!^(.*)/(.*)$!', $sModuleId, $aMatches)) {
|
||||
@@ -380,59 +462,6 @@ class ModuleDiscovery
|
||||
throw new Exception("Data directory (".$sDirectory.") not found or not readable.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<\iTopExtension> $aExtensions
|
||||
* @param string $sModuleName
|
||||
* @param string $sModuleVersion
|
||||
* @param array $aModuleInfo
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function IsModuleInExtensionList(array $aExtensions, string $sModuleName, string $sModuleVersion, array $aModuleInfo): bool
|
||||
{
|
||||
if (count($aExtensions) === 0) {
|
||||
return false;
|
||||
}
|
||||
$aNonMatchingPaths = [];
|
||||
$sModuleFilePath = $aModuleInfo[ModuleFileReader::MODULE_FILE_PATH];
|
||||
|
||||
/** @var \iTopExtension $oExtension */
|
||||
foreach ($aExtensions as $oExtension) {
|
||||
$sCurrentVersion = $oExtension->aModuleVersion[$sModuleName] ?? null;
|
||||
if (is_null($sCurrentVersion)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($sModuleVersion !== $sCurrentVersion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$aCurrentModuleInfo = $oExtension->aModuleInfo[$sModuleName] ?? null;
|
||||
if (is_null($aCurrentModuleInfo)) {
|
||||
SetupLog::Warning("Missing $sModuleName in ".$oExtension->sLabel.". it should not happen");
|
||||
continue;
|
||||
}
|
||||
|
||||
// use case: same module coming from 2 different extensions
|
||||
// we remove only the one coming from removed extensions
|
||||
$sCurrentModuleFilePath = $aCurrentModuleInfo[ModuleFileReader::MODULE_FILE_PATH];
|
||||
if (realpath($sModuleFilePath) !== realpath($sCurrentModuleFilePath)) {
|
||||
$aNonMatchingPaths[] = $sCurrentModuleFilePath;
|
||||
continue;
|
||||
}
|
||||
|
||||
SetupLog::Info("Module considered as removed", null, ['extension_code' => $oExtension->sCode, 'module_name' => $sModuleName, 'module_version' => $sModuleVersion, ModuleFileReader::MODULE_FILE_PATH => $sCurrentModuleFilePath]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (count($aNonMatchingPaths) > 0) {
|
||||
//add log for support
|
||||
SetupLog::Debug("Module kept as it came from non removed extensions", null, ['module_name' => $sModuleName, 'module_version' => $sModuleVersion, ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath, 'non_matching_paths' => $aNonMatchingPaths]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // End of class
|
||||
|
||||
/** Alias for backward compatibility with old module files in which
|
||||
|
||||
@@ -7,15 +7,15 @@ use CoreException;
|
||||
use Exception;
|
||||
use ParseError;
|
||||
use PhpParser\Error;
|
||||
use PhpParser\Node\Arg;
|
||||
use PhpParser\Node\Expr\Array_;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PhpParser\Node\Scalar\String_;
|
||||
use PhpParser\Node\Stmt\ElseIf_;
|
||||
use PhpParser\Node\Stmt\Expression;
|
||||
use PhpParser\Node\Stmt\If_;
|
||||
use PhpParser\ParserFactory;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Stmt\ElseIf_;
|
||||
use PhpParser\Node\Expr\Array_;
|
||||
use PhpParser\Node\Scalar\String_;
|
||||
use PhpParser\Node\Arg;
|
||||
|
||||
require_once __DIR__.'/ModuleFileReaderException.php';
|
||||
require_once APPROOT.'sources/PhpParser/Evaluation/PhpExpressionEvaluator.php';
|
||||
@@ -36,7 +36,6 @@ class ModuleFileReader
|
||||
public const MODULE_INFO_PATH = 0;
|
||||
public const MODULE_INFO_ID = 1;
|
||||
public const MODULE_INFO_CONFIG = 2;
|
||||
public const MODULE_FILE_PATH = "module_file_path";
|
||||
|
||||
public const STATIC_CALLWHITELIST = [
|
||||
"utils::GetItopVersionWikiSyntax",
|
||||
@@ -49,23 +48,21 @@ class ModuleFileReader
|
||||
|
||||
final public static function GetInstance(): ModuleFileReader
|
||||
{
|
||||
if (!isset(self::$oInstance)) {
|
||||
self::$oInstance = new ModuleFileReader();
|
||||
if (!isset(static::$oInstance)) {
|
||||
static::$oInstance = new static();
|
||||
}
|
||||
|
||||
return self::$oInstance;
|
||||
return static::$oInstance;
|
||||
}
|
||||
|
||||
final public static function SetInstance(?ModuleFileReader $oInstance): void
|
||||
{
|
||||
self::$oInstance = $oInstance;
|
||||
static::$oInstance = $oInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the information from a module file (module.xxx.php)
|
||||
*
|
||||
* @param string $sModuleFilePath
|
||||
*
|
||||
* @param string $sModuleFile
|
||||
* @return array
|
||||
* @throws ModuleFileReaderException
|
||||
*/
|
||||
@@ -111,9 +108,7 @@ class ModuleFileReader
|
||||
* Read the information from a module file (module.xxx.php)
|
||||
* Warning: this method is using eval() function to load the ModuleInstallerAPI classes.
|
||||
* Current method is never called at design/runtime. It is acceptable to use it during setup only.
|
||||
*
|
||||
* @param string $sModuleFilePath
|
||||
*
|
||||
* @param string $sModuleFile
|
||||
* @return array
|
||||
* @throws ModuleFileReaderException
|
||||
*/
|
||||
@@ -169,7 +164,7 @@ class ModuleFileReader
|
||||
private function CompleteModuleInfoWithFilePath(array &$aModuleInfo)
|
||||
{
|
||||
if (count($aModuleInfo) == 3) {
|
||||
$aModuleInfo[static::MODULE_INFO_CONFIG][self::MODULE_FILE_PATH] = $aModuleInfo[static::MODULE_INFO_PATH];
|
||||
$aModuleInfo[static::MODULE_INFO_CONFIG]['module_file_path'] = $aModuleInfo[static::MODULE_INFO_PATH];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,21 +175,15 @@ class ModuleFileReader
|
||||
}
|
||||
|
||||
$sModuleInstallerClass = $aModuleInfo['installer'];
|
||||
if (strlen($sModuleInstallerClass) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!class_exists($sModuleInstallerClass)) {
|
||||
$sModuleFilePath = $aModuleInfo[self::MODULE_FILE_PATH];
|
||||
$sModuleFilePath = $aModuleInfo['module_file_path'];
|
||||
$this->ReadModuleFileInformationUnsafe($sModuleFilePath);
|
||||
}
|
||||
|
||||
if (!class_exists($sModuleInstallerClass)) {
|
||||
\IssueLog::Error(__METHOD__, null, $aModuleInfo);
|
||||
throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not a PHP class - Module: ".$aModuleInfo['label']);
|
||||
}
|
||||
if (!is_subclass_of($sModuleInstallerClass, 'ModuleInstallerAPI')) {
|
||||
\IssueLog::Error(__METHOD__, null, $aModuleInfo);
|
||||
throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not derived from 'ModuleInstallerAPI' - Module: ".$aModuleInfo['label']);
|
||||
}
|
||||
|
||||
@@ -203,7 +192,7 @@ class ModuleFileReader
|
||||
|
||||
/**
|
||||
* @param string $sModuleFilePath
|
||||
* @param \PhpParser\Node\Stmt\Expression $oExpression
|
||||
* @param \PhpParser\Node\Expr\Assign $oAssignation
|
||||
*
|
||||
* @return array|null
|
||||
* @throws ModuleFileReaderException
|
||||
|
||||
@@ -89,7 +89,6 @@ class ExtensionInstallation extends cmdbAbstractObject
|
||||
MetaModel::Init_AddAttribute(new AttributeString("source", ["allowed_values" => null, "sql" => "source", "default_value" => null, "is_null_allowed" => false, "depends_on" => []]));
|
||||
MetaModel::Init_AddAttribute(new AttributeEnum("uninstallable", ["allowed_values" => new ValueSetEnum('yes,no,maybe'), "sql" => "uninstallable", "default_value" => 'yes', "is_null_allowed" => false, "depends_on" => []]));
|
||||
MetaModel::Init_AddAttribute(new AttributeDateTime("installed", ["allowed_values" => null, "sql" => "installed", "default_value" => 'NOW()', "is_null_allowed" => false, "depends_on" => []]));
|
||||
MetaModel::Init_AddAttribute(new AttributeText("description", ["allowed_values" => null, "sql" => "description", "default_value" => null, "is_null_allowed" => true, "depends_on" => []]));
|
||||
|
||||
// Display lists
|
||||
MetaModel::Init_SetZListItems('details', ['code', 'label', 'version', 'installed', 'source']); // Attributes to be displayed for the complete details
|
||||
@@ -1,130 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/ModuleInstallationRepository.php';
|
||||
|
||||
class AnalyzeInstallation
|
||||
{
|
||||
private static AnalyzeInstallation $oInstance;
|
||||
private ?array $aAvailableModules = null;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
final public static function GetInstance(): AnalyzeInstallation
|
||||
{
|
||||
if (!isset(self::$oInstance)) {
|
||||
self::$oInstance = new AnalyzeInstallation();
|
||||
}
|
||||
|
||||
return self::$oInstance;
|
||||
}
|
||||
|
||||
final public static function SetInstance(?AnalyzeInstallation $oInstance): void
|
||||
{
|
||||
self::$oInstance = $oInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes the current installation and the possibilities
|
||||
*
|
||||
* @param null|Config $oConfig Defines the target environment (DB)
|
||||
* @param mixed $modulesPath Either a single string or an array of absolute paths
|
||||
* @param bool $bAbortOnMissingDependency ...
|
||||
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
|
||||
*
|
||||
* @return array Array with the following format:
|
||||
* array =>
|
||||
* 'iTop' => array(
|
||||
* 'installed_version' => ... (could be empty in case of a fresh install)
|
||||
* 'available_version => ...
|
||||
* )
|
||||
* <module_name> => array(
|
||||
* 'installed_version' => ...
|
||||
* 'available_version' => ...
|
||||
* 'install' => array(
|
||||
* 'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
|
||||
* 'message' => ...
|
||||
* )
|
||||
* 'uninstall' => array(
|
||||
* 'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
|
||||
* 'message' => ...
|
||||
* )
|
||||
* 'label' => ...
|
||||
* 'dependencies' => array(<module1>, <module2>, ...)
|
||||
* 'visible' => true | false
|
||||
* )
|
||||
* )
|
||||
* @throws \Exception
|
||||
*/
|
||||
|
||||
public function AnalyzeInstallation(?Config $oConfig, mixed $modulesPath, bool $bAbortOnMissingDependency = false, ?array $aModulesToLoad = null)
|
||||
{
|
||||
$aRes = [
|
||||
ROOT_MODULE => [
|
||||
'installed_version' => '',
|
||||
'available_version' => ITOP_VERSION_FULL,
|
||||
'name_code' => ITOP_APPLICATION,
|
||||
],
|
||||
];
|
||||
|
||||
$aDirs = is_array($modulesPath) ? $modulesPath : [$modulesPath];
|
||||
if (! is_null($this->aAvailableModules)) {
|
||||
//test only
|
||||
$aAvailableModules = $this->aAvailableModules;
|
||||
} else {
|
||||
$aAvailableModules = ModuleDiscovery::GetModulesOrderedByDependencies($aDirs, $bAbortOnMissingDependency, $aModulesToLoad);
|
||||
}
|
||||
|
||||
foreach ($aAvailableModules as $sModuleId => $aModuleInfo) {
|
||||
list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
|
||||
|
||||
$aModuleInfo['installed_version'] = '';
|
||||
$aModuleInfo['available_version'] = $sModuleVersion;
|
||||
|
||||
if ($aModuleInfo['mandatory']) {
|
||||
$aModuleInfo['install'] = [
|
||||
'flag' => MODULE_ACTION_MANDATORY,
|
||||
'message' => 'the module is part of the application',
|
||||
];
|
||||
} else {
|
||||
$aModuleInfo['install'] = [
|
||||
'flag' => MODULE_ACTION_OPTIONAL,
|
||||
'message' => '',
|
||||
];
|
||||
}
|
||||
$aRes[$sModuleName] = $aModuleInfo;
|
||||
}
|
||||
|
||||
$aCurrentlyInstalledModules = ModuleInstallationRepository::GetInstance()->ReadComputeInstalledModules($oConfig);
|
||||
|
||||
// Adjust the list of proposed modules
|
||||
foreach ($aCurrentlyInstalledModules as $sModuleName => $aModuleDB) {
|
||||
if ($sModuleName == ROOT_MODULE) {
|
||||
$aRes[$sModuleName]['installed_version'] = $aModuleDB['version'];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!array_key_exists($sModuleName, $aRes)) {
|
||||
// A module was installed, it is not proposed in the new build... skip
|
||||
continue;
|
||||
}
|
||||
|
||||
$aRes[$sModuleName]['installed_version'] = $aModuleDB['version'];
|
||||
|
||||
if ($aRes[$sModuleName]['mandatory']) {
|
||||
$aRes[$sModuleName]['uninstall'] = [
|
||||
'flag' => MODULE_ACTION_IMPOSSIBLE,
|
||||
'message' => 'the module is part of the application',
|
||||
];
|
||||
} else {
|
||||
$aRes[$sModuleName]['uninstall'] = [
|
||||
'flag' => MODULE_ACTION_OPTIONAL,
|
||||
'message' => '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $aRes;
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Setup\ModuleDependency\DependencyExpression;
|
||||
|
||||
require_once __DIR__.'/ModuleInstallationException.php';
|
||||
require_once(APPROOT.'/setup/moduledependency/module.class.inc.php');
|
||||
|
||||
class InstallationChoicesToModuleConverter
|
||||
{
|
||||
private static ?InstallationChoicesToModuleConverter $oInstance;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
final public static function GetInstance(): InstallationChoicesToModuleConverter
|
||||
{
|
||||
if (!isset(self::$oInstance)) {
|
||||
self::$oInstance = new InstallationChoicesToModuleConverter();
|
||||
}
|
||||
|
||||
return self::$oInstance;
|
||||
}
|
||||
|
||||
final public static function SetInstance(?InstallationChoicesToModuleConverter $oInstance): void
|
||||
{
|
||||
self::$oInstance = $oInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aInstallationChoices
|
||||
* @param array $aSearchDirs
|
||||
*
|
||||
* @return array
|
||||
* @throws \ModuleInstallationException
|
||||
*/
|
||||
public function GetModules(array $aInstallationChoices, array $aSearchDirs, ?string $sInstallationFilePath = null): array
|
||||
{
|
||||
$aPackageModules = ModuleDiscovery::GetAllModules($aSearchDirs);
|
||||
|
||||
$bInstallationFileProvided = ! is_null($sInstallationFilePath) && is_file($sInstallationFilePath);
|
||||
|
||||
if ($bInstallationFileProvided) {
|
||||
$oXMLParameters = new XMLParameters($sInstallationFilePath);
|
||||
$aSteps = $oXMLParameters->Get('steps', []);
|
||||
if (!is_array($aSteps)) {
|
||||
return [];
|
||||
}
|
||||
$aInstalledModuleNames = $this->FindInstalledPackageModules($aPackageModules, $aInstallationChoices, $aSteps);
|
||||
} else {
|
||||
$aInstalledModuleNames = $this->FindInstalledPackageModules($aPackageModules, $aInstallationChoices);
|
||||
}
|
||||
|
||||
$aInstalledModules = [];
|
||||
foreach (array_keys($aPackageModules) as $sModuleId) {
|
||||
list($sModuleName) = ModuleDiscovery::GetModuleName($sModuleId);
|
||||
if (in_array($sModuleName, $aInstalledModuleNames)) {
|
||||
$aInstalledModules[] = $sModuleId;
|
||||
}
|
||||
}
|
||||
|
||||
return $aInstalledModules;
|
||||
}
|
||||
|
||||
private function FindInstalledPackageModules(array $aPackageModules, array $aInstallationChoices, array $aInstallationDescription = null): array
|
||||
{
|
||||
$aInstalledModules = [];
|
||||
|
||||
$this->ProcessDefaultModules($aPackageModules, $aInstalledModules);
|
||||
|
||||
if (is_null($aInstallationDescription)) {
|
||||
//in legacy usecase: choices are flat modules list already
|
||||
foreach ($aInstallationChoices as $sModuleName) {
|
||||
$aInstalledModules[$sModuleName] = true;
|
||||
}
|
||||
} else {
|
||||
$this->GetModuleNamesFromInstallationChoices($aInstallationChoices, $aInstallationDescription, $aInstalledModules);
|
||||
}
|
||||
|
||||
$this->ProcessAutoSelectModules($aPackageModules, $aInstalledModules);
|
||||
|
||||
return array_keys($aInstalledModules);
|
||||
}
|
||||
|
||||
private function IsDefaultModule(string $sModuleId, array $aModule): bool
|
||||
{
|
||||
if (($sModuleId === ROOT_MODULE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($aModule['auto_select'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($aModule['category'] === 'authentication') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !$aModule['visible'];
|
||||
}
|
||||
|
||||
private function ProcessDefaultModules(array &$aPackageModules, array &$aInstalledModules): void
|
||||
{
|
||||
foreach ($aPackageModules as $sModuleId => $aModule) {
|
||||
if ($this->IsDefaultModule($sModuleId, $aModule)) {
|
||||
list($sModuleName) = ModuleDiscovery::GetModuleName($sModuleId);
|
||||
$aInstalledModules[$sModuleName] = true;
|
||||
unset($aPackageModules[$sModuleId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function IsAutoSelectedModule(array $aInstalledModules, string $sModuleId, array $aModule): bool
|
||||
{
|
||||
if (($sModuleId === ROOT_MODULE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($aModule['auto_select'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
SetupInfo::SetSelectedModules($aInstalledModules);
|
||||
return DependencyExpression::GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($aModule['auto_select']);
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Error('Error evaluating module auto-select', null, [
|
||||
'module' => $sModuleId,
|
||||
'error' => $e->getMessage(),
|
||||
'evaluated code' => $aModule['auto_select'],
|
||||
'stacktrace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function ProcessAutoSelectModules(array $aPackageModules, array &$aInstalledModules): void
|
||||
{
|
||||
foreach ($aPackageModules as $sModuleId => $aModule) {
|
||||
if ($this->IsAutoSelectedModule($aInstalledModules, $sModuleId, $aModule)) {
|
||||
list($sModuleName) = ModuleDiscovery::GetModuleName($sModuleId);
|
||||
$aInstalledModules[$sModuleName] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function GetModuleNamesFromInstallationChoices(array $aInstallationChoices, array $aInstallationDescription, array &$aModuleNames): void
|
||||
{
|
||||
foreach ($aInstallationDescription as $aStepInfo) {
|
||||
$aOptions = $aStepInfo['options'] ?? null;
|
||||
if (is_array($aOptions)) {
|
||||
foreach ($aOptions as $aChoiceInfo) {
|
||||
$this->ProcessSelectedChoice($aInstallationChoices, $aChoiceInfo, $aModuleNames);
|
||||
}
|
||||
}
|
||||
$aOptions = $aStepInfo['alternatives'] ?? null;
|
||||
if (is_array($aOptions)) {
|
||||
foreach ($aOptions as $aChoiceInfo) {
|
||||
$this->ProcessSelectedChoice($aInstallationChoices, $aChoiceInfo, $aModuleNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function ProcessSelectedChoice(array $aInstallationChoices, array $aChoiceInfo, array &$aInstalledModules)
|
||||
{
|
||||
if (!is_array($aChoiceInfo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sMandatory = $aChoiceInfo['mandatory'] ?? 'false';
|
||||
|
||||
$aCurrentModules = $aChoiceInfo['modules'] ?? [];
|
||||
$sExtensionCode = $aChoiceInfo['extension_code'];
|
||||
|
||||
$bSelected = ($sMandatory === 'true') || in_array($sExtensionCode, $aInstallationChoices);
|
||||
|
||||
if (!$bSelected) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($aCurrentModules as $sModuleId) {
|
||||
$aInstalledModules[$sModuleId] = true;
|
||||
}
|
||||
|
||||
$aAlternatives = $aChoiceInfo['alternatives'] ?? null;
|
||||
if (is_array($aAlternatives)) {
|
||||
foreach ($aAlternatives as $aSubChoiceInfo) {
|
||||
$this->ProcessSelectedChoice($aInstallationChoices, $aSubChoiceInfo, $aInstalledModules);
|
||||
}
|
||||
}
|
||||
|
||||
$aSubOptionsChoiceInfo = $aChoiceInfo['sub_options'] ?? null;
|
||||
if (is_array($aSubOptionsChoiceInfo)) {
|
||||
$aSubOptions = $aSubOptionsChoiceInfo['options'] ?? null;
|
||||
if (is_array($aSubOptions)) {
|
||||
foreach ($aSubOptions as $aSubChoiceInfo) {
|
||||
$this->ProcessSelectedChoice($aInstallationChoices, $aSubChoiceInfo, $aInstalledModules);
|
||||
}
|
||||
}
|
||||
$aSubAlternatives = $aSubOptionsChoiceInfo['alternatives'] ?? null;
|
||||
if (is_array($aSubAlternatives)) {
|
||||
foreach ($aSubAlternatives as $aSubChoiceInfo) {
|
||||
$this->ProcessSelectedChoice($aInstallationChoices, $aSubChoiceInfo, $aInstalledModules);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ModuleInstallationException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ModuleInstallationRepository
|
||||
{
|
||||
private static ModuleInstallationRepository $oInstance;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
final public static function GetInstance(): ModuleInstallationRepository
|
||||
{
|
||||
if (!isset(self::$oInstance)) {
|
||||
self::$oInstance = new ModuleInstallationRepository();
|
||||
}
|
||||
|
||||
return self::$oInstance;
|
||||
}
|
||||
|
||||
final public static function SetInstance(?ModuleInstallationRepository $oInstance): void
|
||||
{
|
||||
self::$oInstance = $oInstance;
|
||||
}
|
||||
|
||||
private ?array $aSelectInstall = null;
|
||||
|
||||
/**
|
||||
* @param \Config|null $oConfig
|
||||
* @return array
|
||||
*/
|
||||
public function ReadComputeInstalledModules(?Config $oConfig): array
|
||||
{
|
||||
$aSelectInstall = [];
|
||||
try {
|
||||
$aSelectInstall = $this->ReadFromDB($oConfig);
|
||||
} catch (MySQLException $e) {
|
||||
// No database or erroneous information
|
||||
}
|
||||
|
||||
return $this->ComputeInstalledModules($aSelectInstall);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Config|null $oConfig
|
||||
* @return array
|
||||
* @throws \MySQLException
|
||||
* @throws \MySQLQueryHasNoResultException
|
||||
*/
|
||||
public function ReadFromDB(?Config $oConfig): array
|
||||
{
|
||||
if (is_null($oConfig)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! is_null($this->aSelectInstall)) {
|
||||
//test only
|
||||
return $this->aSelectInstall;
|
||||
}
|
||||
|
||||
CMDBSource::InitFromConfig($oConfig);
|
||||
//read db module installations
|
||||
$tableWithPrefix = $this->GetTableWithPrefix($oConfig);
|
||||
$iRootId = CMDBSource::QueryToScalar("SELECT max(parent_id) FROM $tableWithPrefix");
|
||||
// Get the latest installed modules, without the "root" ones (iTop version and datamodel version)
|
||||
$sSQL = <<<SQL
|
||||
SELECT * FROM $tableWithPrefix
|
||||
WHERE
|
||||
parent_id='$iRootId'
|
||||
OR id='$iRootId'
|
||||
SQL;
|
||||
return CMDBSource::QueryToArray($sSQL);
|
||||
}
|
||||
|
||||
private function GetTableWithPrefix(Config $oConfig)
|
||||
{
|
||||
$sPrefix = $oConfig->Get('db_subname');
|
||||
if (utils::IsNullOrEmptyString($sPrefix)) {
|
||||
return "priv_module_install";
|
||||
}
|
||||
|
||||
return "{$sPrefix}priv_module_install";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Config $oConfig
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function GetApplicationVersion(Config $oConfig)
|
||||
{
|
||||
try {
|
||||
CMDBSource::InitFromConfig($oConfig);
|
||||
$tableWithPrefix = $this->GetTableWithPrefix($oConfig);
|
||||
$sSQLQuery = "SELECT * FROM $tableWithPrefix";
|
||||
$aSelectInstall = CMDBSource::QueryToArray($sSQLQuery);
|
||||
} catch (MySQLException $e) {
|
||||
// No database or erroneous information
|
||||
SetupLog::Error(
|
||||
'Can not connect to the database',
|
||||
null,
|
||||
[
|
||||
'host' => $oConfig->Get('db_host'),
|
||||
'user' => $oConfig->Get('db_user'),
|
||||
'pwd:' => $oConfig->Get('db_pwd'),
|
||||
'db name' => $oConfig->Get('db_name'),
|
||||
'msg' => $e->getMessage(),
|
||||
]
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$aResult = [];
|
||||
// Scan the list of installed modules to get the version of the 'ROOT' module which holds the main application version
|
||||
foreach ($aSelectInstall as $aInstall) {
|
||||
$sModuleVersion = $aInstall['version'];
|
||||
if ($sModuleVersion == '') {
|
||||
// Though the version cannot be empty in iTop 2.0, it used to be possible
|
||||
// therefore we have to put something here or the module will not be considered
|
||||
// as being installed
|
||||
$sModuleVersion = '0.0.0';
|
||||
}
|
||||
|
||||
if ($aInstall['parent_id'] == 0) {
|
||||
if ($aInstall['name'] == DATAMODEL_MODULE) {
|
||||
$aResult['datamodel_version'] = $sModuleVersion;
|
||||
$aComments = json_decode($aInstall['comment'], true);
|
||||
if (is_array($aComments)) {
|
||||
$aResult = array_merge($aResult, $aComments);
|
||||
}
|
||||
} else {
|
||||
$aResult['product_name'] = $aInstall['name'];
|
||||
$aResult['product_version'] = $sModuleVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!array_key_exists('datamodel_version', $aResult)) {
|
||||
// Versions prior to 2.0 did not record the version of the datamodel
|
||||
// so assume that the datamodel version is equal to the application version
|
||||
$aResult['datamodel_version'] = $aResult['product_version'];
|
||||
}
|
||||
|
||||
SetupLog::Info(__METHOD__, null, ["product_name" => $aResult['product_name'], "product_version" => $aResult['product_version']]);
|
||||
|
||||
return count($aResult) == 0 ? false : $aResult;
|
||||
}
|
||||
|
||||
private function ComputeInstalledModules(array $aSelectInstall): array
|
||||
{
|
||||
$aInstallByModule = []; // array of <module> => array ('installed' => timestamp, 'version' => <version>)
|
||||
|
||||
//module installation datetime is mostly the same for all modules
|
||||
//unless there was issue recording things in DB
|
||||
$sFirstDatetime = null;
|
||||
$iFirstTime = -1;
|
||||
foreach ($aSelectInstall as $aInstall) {
|
||||
//$aInstall['comment']; // unsused
|
||||
$sDatetime = $aInstall['installed'];
|
||||
|
||||
if (is_null($sFirstDatetime)) {
|
||||
$sFirstDatetime = $sDatetime;
|
||||
$iFirstTime = strtotime($sDatetime);
|
||||
$iInstalled = $iFirstTime;
|
||||
} elseif ($sDatetime === $sFirstDatetime) {
|
||||
$iInstalled = $iFirstTime;
|
||||
} else {
|
||||
$sDatetime = $aInstall['installed'];
|
||||
$iInstalled = strtotime($sDatetime);
|
||||
}
|
||||
|
||||
$sModuleName = $aInstall['name'];
|
||||
$sModuleVersion = $aInstall['version'];
|
||||
if ($sModuleVersion == '') {
|
||||
// Though the version cannot be empty in iTop 2.0, it used to be possible
|
||||
// therefore we have to put something here or the module will not be considered
|
||||
// as being installed
|
||||
$sModuleVersion = '0.0.0';
|
||||
}
|
||||
|
||||
if ($aInstall['parent_id'] == 0) {
|
||||
$aInstallByModule[ROOT_MODULE] = [
|
||||
'installed_version' => $sModuleVersion,
|
||||
'installed' => $iInstalled,
|
||||
'version' => $sModuleVersion,
|
||||
];
|
||||
} else {
|
||||
$aInstallByModule[$sModuleName] = [
|
||||
'installed' => $iInstalled,
|
||||
'version' => $sModuleVersion,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $aInstallByModule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return previous module installation. offset is applied on parent_id.
|
||||
* @param $iOffset: by default (offset=0) returns current installation
|
||||
* @return array
|
||||
*/
|
||||
public static function GetPreviousModuleInstallationsByOffset(int $iOffset = 0): array
|
||||
{
|
||||
$oFilter = DBObjectSearch::FromOQL('SELECT ModuleInstallation AS mi WHERE mi.parent_id=0 AND mi.name!="datamodel"');
|
||||
$oSet = new DBObjectSet($oFilter, ['installed' => false]); // Most recent first
|
||||
$oSet->SetLimit($iOffset + 1);
|
||||
|
||||
$iParentId = 0;
|
||||
while (!is_null($oModuleInstallation = $oSet->Fetch())) {
|
||||
/** @var \DBObject $oModuleInstallation */
|
||||
if ($iOffset == 0) {
|
||||
$iParentId = $oModuleInstallation->Get('id');
|
||||
break;
|
||||
}
|
||||
$iOffset--;
|
||||
}
|
||||
|
||||
if ($iParentId === 0) {
|
||||
IssueLog::Error("no ITOP_APPLICATION ModuleInstallation found", null, ['offset' => $iOffset]);
|
||||
throw new \Exception("no ITOP_APPLICATION ModuleInstallation found");
|
||||
}
|
||||
|
||||
$oFilter = DBObjectSearch::FromOQL("SELECT ModuleInstallation AS mi WHERE mi.id=$iParentId OR mi.parent_id=$iParentId");
|
||||
$oSet = new DBObjectSet($oFilter); // Most recent first
|
||||
$aRawValues = $oSet->ToArrayOfValues();
|
||||
$aValues = [];
|
||||
foreach ($aRawValues as $aRawValue) {
|
||||
$aValue = [];
|
||||
foreach ($aRawValue as $sAliasAttCode => $sValue) {
|
||||
// remove 'mi.' from AttCode
|
||||
$sAttCode = substr($sAliasAttCode, 3);
|
||||
$aValue[$sAttCode] = $sValue;
|
||||
}
|
||||
|
||||
$aValues[] = $aValue;
|
||||
}
|
||||
return $aValues;
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,6 @@ abstract class ModuleInstallerAPI
|
||||
if (in_array($sTo, $aNewValues)) {
|
||||
$sEnumCol = $oAttDef->Get("sql");
|
||||
$aFields = CMDBSource::QueryToArray("SHOW COLUMNS FROM `$sTableName` WHERE Field = '$sEnumCol'");
|
||||
$aCurrentValues = [];
|
||||
if (isset($aFields[0]['Type'])) {
|
||||
$sColType = $aFields[0]['Type'];
|
||||
// Note: the parsing should rely on str_getcsv (requires PHP 5.3) to cope with escaped string
|
||||
|
||||
@@ -7,7 +7,6 @@ class InvalidParameterException extends Exception
|
||||
abstract class Parameters
|
||||
{
|
||||
public $aData = null;
|
||||
private ?array $aParamValues = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -27,26 +26,24 @@ abstract class Parameters
|
||||
*/
|
||||
public function GetParamForConfigArray()
|
||||
{
|
||||
if (is_null($this->aParamValues)) {
|
||||
$aDBParams = $this->Get('database');
|
||||
$this->aParamValues = [
|
||||
'mode' => $this->Get('mode'),
|
||||
'db_server' => $aDBParams['server'],
|
||||
'db_user' => $aDBParams['user'],
|
||||
'db_pwd' => $aDBParams['pwd'],
|
||||
'db_name' => $aDBParams['name'],
|
||||
'new_db_name' => $aDBParams['name'],
|
||||
'db_prefix' => $aDBParams['prefix'],
|
||||
'db_tls_enabled' => $aDBParams['db_tls_enabled'],
|
||||
'db_tls_ca' => $aDBParams['db_tls_ca'],
|
||||
'application_path' => $this->Get('url', ''),
|
||||
'language' => $this->Get('language', ''),
|
||||
'graphviz_path' => $this->Get('graphviz_path', ''),
|
||||
'source_dir' => $this->Get('source_dir', ''),
|
||||
];
|
||||
}
|
||||
$aDBParams = $this->Get('database');
|
||||
$aParamValues = [
|
||||
'mode' => $this->Get('mode'),
|
||||
'db_server' => $aDBParams['server'],
|
||||
'db_user' => $aDBParams['user'],
|
||||
'db_pwd' => $aDBParams['pwd'],
|
||||
'db_name' => $aDBParams['name'],
|
||||
'new_db_name' => $aDBParams['name'],
|
||||
'db_prefix' => $aDBParams['prefix'],
|
||||
'db_tls_enabled' => $aDBParams['db_tls_enabled'],
|
||||
'db_tls_ca' => $aDBParams['db_tls_ca'],
|
||||
'application_path' => $this->Get('url', ''),
|
||||
'language' => $this->Get('language', ''),
|
||||
'graphviz_path' => $this->Get('graphviz_path', ''),
|
||||
'source_dir' => $this->Get('source_dir', ''),
|
||||
];
|
||||
|
||||
return $this->aParamValues;
|
||||
return $aParamValues;
|
||||
}
|
||||
|
||||
public function Set($sCode, $value)
|
||||
@@ -107,9 +104,7 @@ class PHPParameters extends Parameters
|
||||
{
|
||||
if ($this->aData == null) {
|
||||
require_once($sParametersFile);
|
||||
if (isset($ITOP_PARAMS)) {
|
||||
$this->aData = $ITOP_PARAMS; // Defined in the file loaded just above
|
||||
}
|
||||
$this->aData = $ITOP_PARAMS; // Defined in the file loaded just above
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,972 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2013-2026 Combodo SAS
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
require_once(APPROOT.'setup/parameters.class.inc.php');
|
||||
require_once(APPROOT.'setup/xmldataloader.class.inc.php');
|
||||
require_once(APPROOT.'setup/backup.class.inc.php');
|
||||
|
||||
require_once(APPROOT.'setup/sequencers/StepSequencer.php');
|
||||
require_once(APPROOT.'setup/SetupDBBackup.php');
|
||||
|
||||
/**
|
||||
* The base class for the installation process.
|
||||
* The installation process is split into a sequence of unitary steps
|
||||
* for performance reasons (i.e; timeout, memory usage) and also in order
|
||||
* to provide some feedback about the progress of the installation.
|
||||
*
|
||||
* This class can be used for a step by step interactive installation
|
||||
* while displaying a progress bar, or in an unattended manner
|
||||
* (for example from the command line), to run all the steps
|
||||
* in one go.
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
class ApplicationInstallSequencer extends StepSequencer
|
||||
{
|
||||
/** @var \Parameters */
|
||||
protected $oParams;
|
||||
protected static $bMetaModelStarted = false;
|
||||
|
||||
protected Config $oConfig;
|
||||
|
||||
/**
|
||||
* @param \Parameters $oParams
|
||||
*
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public function __construct($oParams)
|
||||
{
|
||||
$this->oParams = $oParams;
|
||||
|
||||
$aParamValues = $oParams->GetParamForConfigArray();
|
||||
$this->oConfig = new Config();
|
||||
$this->oConfig->UpdateFromParams($aParamValues);
|
||||
utils::SetConfig($this->oConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function GetTargetEnv()
|
||||
{
|
||||
$sTargetEnvironment = $this->oParams->Get('target_env', '');
|
||||
if ($sTargetEnvironment !== '') {
|
||||
return $sTargetEnvironment;
|
||||
}
|
||||
|
||||
return 'production';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function GetTargetDir()
|
||||
{
|
||||
$sTargetEnv = $this->GetTargetEnv();
|
||||
return 'env-'.$sTargetEnv;
|
||||
}
|
||||
|
||||
protected function GetConfig()
|
||||
{
|
||||
$sTargetEnvironment = $this->GetTargetEnv();
|
||||
$sConfigFile = APPCONF.$sTargetEnvironment.'/'.ITOP_CONFIG_FILE;
|
||||
try {
|
||||
$oConfig = new Config($sConfigFile);
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$aParamValues = $this->oParams->GetParamForConfigArray();
|
||||
$oConfig->UpdateFromParams($aParamValues);
|
||||
|
||||
return $oConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the next step of the installation and reports about the progress
|
||||
* and the next step to perform
|
||||
*
|
||||
* @param string $sStep The identifier of the step to execute
|
||||
* @param string|null $sInstallComment
|
||||
*
|
||||
* @return array (status => , message => , percentage-completed => , next-step => , next-step-label => )
|
||||
*/
|
||||
public function ExecuteStep($sStep = '', $sInstallComment = null)
|
||||
{
|
||||
try {
|
||||
$fStart = microtime(true);
|
||||
SetupLog::Info("##### STEP {$sStep} start");
|
||||
$this->EnterReadOnlyMode();
|
||||
switch ($sStep) {
|
||||
case '':
|
||||
$aResult = [
|
||||
'status' => self::OK,
|
||||
'message' => '',
|
||||
'percentage-completed' => 0,
|
||||
'next-step' => 'copy',
|
||||
'next-step-label' => 'Copying data model files',
|
||||
];
|
||||
|
||||
// Log the parameters...
|
||||
$oDoc = new DOMDocument('1.0', 'UTF-8');
|
||||
$oDoc->preserveWhiteSpace = false;
|
||||
$oDoc->formatOutput = true;
|
||||
$this->oParams->ToXML($oDoc, null, 'installation');
|
||||
$sXML = $oDoc->saveXML();
|
||||
$sSafeXml = preg_replace("|<pwd>([^<]*)</pwd>|", "<pwd>**removed**</pwd>", $sXML);
|
||||
SetupLog::Info("======= Installation starts =======\nParameters:\n$sSafeXml\n");
|
||||
|
||||
// Save the response file as a stand-alone file as well
|
||||
$sFileName = 'install-'.date('Y-m-d');
|
||||
$index = 0;
|
||||
while (file_exists(APPROOT.'log/'.$sFileName.'.xml')) {
|
||||
$index++;
|
||||
$sFileName = 'install-'.date('Y-m-d').'-'.$index;
|
||||
}
|
||||
file_put_contents(APPROOT.'log/'.$sFileName.'.xml', $sSafeXml);
|
||||
|
||||
break;
|
||||
|
||||
case 'copy':
|
||||
$aPreinstall = $this->oParams->Get('preinstall');
|
||||
$aCopies = $aPreinstall['copies'] ?? [];
|
||||
|
||||
$this->DoCopy($aCopies);
|
||||
$sReport = "Copying...";
|
||||
|
||||
$aResult = [
|
||||
'status' => self::OK,
|
||||
'message' => $sReport,
|
||||
];
|
||||
if (isset($aPreinstall['backup'])) {
|
||||
$aResult['next-step'] = 'backup';
|
||||
$aResult['next-step-label'] = 'Performing a backup of the database';
|
||||
$aResult['percentage-completed'] = 20;
|
||||
} else {
|
||||
$aResult['next-step'] = 'compile';
|
||||
$aResult['next-step-label'] = 'Compiling the data model';
|
||||
$aResult['percentage-completed'] = 20;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'backup':
|
||||
$aPreinstall = $this->oParams->Get('preinstall');
|
||||
// __DB__-%Y-%m-%d
|
||||
$sDestination = $aPreinstall['backup']['destination'];
|
||||
$sSourceConfigFile = $aPreinstall['backup']['configuration_file'];
|
||||
$sMySQLBinDir = $this->oParams->Get('mysql_bindir', null);
|
||||
$this->DoBackup($sDestination, $sSourceConfigFile, $sMySQLBinDir);
|
||||
|
||||
$aResult = [
|
||||
'status' => self::OK,
|
||||
'message' => "Created backup",
|
||||
'next-step' => 'compile',
|
||||
'next-step-label' => 'Compiling the data model',
|
||||
'percentage-completed' => 20,
|
||||
];
|
||||
break;
|
||||
|
||||
case 'compile':
|
||||
$aSelectedModules = $this->oParams->Get('selected_modules');
|
||||
$sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest');
|
||||
$sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions');
|
||||
$aMiscOptions = $this->oParams->Get('options', []);
|
||||
$aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', []);
|
||||
|
||||
$bUseSymbolicLinks = null;
|
||||
if ((isset($aMiscOptions['symlinks']) && $aMiscOptions['symlinks'])) {
|
||||
if (function_exists('symlink')) {
|
||||
$bUseSymbolicLinks = true;
|
||||
SetupLog::Info("Using symbolic links instead of copying data model files (for developers only!)");
|
||||
} else {
|
||||
SetupLog::Info("Symbolic links (function symlinks) does not seem to be supported on this platform (OS/PHP version).");
|
||||
}
|
||||
}
|
||||
|
||||
$this->DoCompile(
|
||||
$aRemovedExtensionCodes,
|
||||
$aSelectedModules,
|
||||
$sSourceDir,
|
||||
$sExtensionDir,
|
||||
$bUseSymbolicLinks
|
||||
);
|
||||
|
||||
$sNextStep = 'db-schema';
|
||||
$sNextStepLabel = 'Updating database schema';
|
||||
|
||||
$aResult = [
|
||||
'status' => self::OK,
|
||||
'message' => '',
|
||||
'next-step' => $sNextStep,
|
||||
'next-step-label' => $sNextStepLabel,
|
||||
'percentage-completed' => 40,
|
||||
];
|
||||
break;
|
||||
|
||||
case 'db-schema':
|
||||
$aSelectedModules = $this->oParams->Get('selected_modules', []);
|
||||
|
||||
$this->DoUpdateDBSchema(
|
||||
$aSelectedModules
|
||||
);
|
||||
|
||||
$aResult = [
|
||||
'status' => self::OK,
|
||||
'message' => '',
|
||||
'next-step' => 'after-db-create',
|
||||
'next-step-label' => 'Creating profiles',
|
||||
'percentage-completed' => 60,
|
||||
];
|
||||
break;
|
||||
|
||||
case 'after-db-create':
|
||||
$aAdminParams = $this->oParams->Get('admin_account');
|
||||
$sAdminUser = $aAdminParams['user'];
|
||||
$sAdminPwd = $aAdminParams['pwd'];
|
||||
$sAdminLanguage = $aAdminParams['language'];
|
||||
$aSelectedModules = $this->oParams->Get('selected_modules', []);
|
||||
|
||||
$this->AfterDBCreate(
|
||||
$sAdminUser,
|
||||
$sAdminPwd,
|
||||
$sAdminLanguage,
|
||||
$aSelectedModules
|
||||
);
|
||||
|
||||
$aResult = [
|
||||
'status' => self::OK,
|
||||
'message' => '',
|
||||
'next-step' => 'load-data',
|
||||
'next-step-label' => 'Loading data',
|
||||
'percentage-completed' => 80,
|
||||
];
|
||||
break;
|
||||
|
||||
case 'load-data':
|
||||
$aSelectedModules = $this->oParams->Get('selected_modules');
|
||||
$bSampleData = ($this->oParams->Get('sample_data', 0) == 1);
|
||||
|
||||
$this->DoLoadFiles(
|
||||
$aSelectedModules,
|
||||
$bSampleData
|
||||
);
|
||||
|
||||
$aResult = [
|
||||
'status' => self::INFO,
|
||||
'message' => 'All data loaded',
|
||||
'next-step' => 'create-config',
|
||||
'next-step-label' => 'Creating the configuration File',
|
||||
'percentage-completed' => 99,
|
||||
];
|
||||
break;
|
||||
|
||||
case 'create-config':
|
||||
$sPreviousConfigFile = $this->oParams->Get('previous_configuration_file', '');
|
||||
$sDataModelVersion = $this->oParams->Get('datamodel_version', '0.0.0');
|
||||
$aSelectedModuleCodes = $this->oParams->Get('selected_modules', []);
|
||||
$aSelectedExtensionCodes = $this->oParams->Get('selected_extensions', []);
|
||||
|
||||
$this->DoCreateConfig(
|
||||
$sPreviousConfigFile,
|
||||
$sDataModelVersion,
|
||||
$aSelectedModuleCodes,
|
||||
$aSelectedExtensionCodes,
|
||||
$sInstallComment
|
||||
);
|
||||
|
||||
$aResult = [
|
||||
'status' => self::INFO,
|
||||
'message' => 'Configuration file created',
|
||||
'next-step' => '',
|
||||
'next-step-label' => 'Completed',
|
||||
'percentage-completed' => 100,
|
||||
];
|
||||
$this->ExitReadOnlyMode();
|
||||
break;
|
||||
|
||||
default:
|
||||
$aResult = [
|
||||
'status' => self::ERROR,
|
||||
'message' => '',
|
||||
'next-step' => '',
|
||||
'next-step-label' => "Unknown setup step '$sStep'.",
|
||||
'percentage-completed' => 100,
|
||||
];
|
||||
break;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$aResult = [
|
||||
'status' => self::ERROR,
|
||||
'message' => $e->getMessage(),
|
||||
'next-step' => '',
|
||||
'next-step-label' => '',
|
||||
'percentage-completed' => 100,
|
||||
];
|
||||
|
||||
SetupLog::Error('An exception occurred: '.$e->getMessage().' at line '.$e->getLine().' in file '.$e->getFile());
|
||||
$idx = 0;
|
||||
// Log the call stack, but not the parameters since they may contain passwords or other sensitive data
|
||||
SetupLog::Ok("Call stack:");
|
||||
foreach ($e->getTrace() as $aTrace) {
|
||||
$sLine = empty($aTrace['line']) ? "" : $aTrace['line'];
|
||||
$sFile = empty($aTrace['file']) ? "" : $aTrace['file'];
|
||||
$sClass = empty($aTrace['class']) ? "" : $aTrace['class'];
|
||||
$sType = empty($aTrace['type']) ? "" : $aTrace['type'];
|
||||
$sFunction = empty($aTrace['function']) ? "" : $aTrace['function'];
|
||||
$sVerb = empty($sClass) ? $sFunction : "$sClass{$sType}$sFunction";
|
||||
SetupLog::Ok("#$idx $sFile($sLine): $sVerb(...)");
|
||||
$idx++;
|
||||
}
|
||||
} finally {
|
||||
$fDuration = round(microtime(true) - $fStart, 2);
|
||||
SetupLog::Info("##### STEP {$sStep} duration: {$fDuration}s");
|
||||
}
|
||||
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
protected function EnterReadOnlyMode()
|
||||
{
|
||||
if ($this->GetTargetEnv() != 'production') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SetupUtils::IsInReadOnlyMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetupUtils::EnterReadOnlyMode($this->GetConfig());
|
||||
}
|
||||
|
||||
protected function ExitReadOnlyMode()
|
||||
{
|
||||
if ($this->GetTargetEnv() != 'production') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SetupUtils::IsInReadOnlyMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetupUtils::ExitReadOnlyMode();
|
||||
}
|
||||
|
||||
protected function DoCopy($aCopies)
|
||||
{
|
||||
$aReports = [];
|
||||
foreach ($aCopies as $aCopy) {
|
||||
$sSource = $aCopy['source'];
|
||||
$sDestination = APPROOT.$aCopy['destination'];
|
||||
|
||||
SetupUtils::builddir($sDestination);
|
||||
SetupUtils::tidydir($sDestination);
|
||||
SetupUtils::copydir($sSource, $sDestination);
|
||||
$aReports[] = "'{$aCopy['source']}' to '{$aCopy['destination']}' (OK)";
|
||||
}
|
||||
if (count($aReports) > 0) {
|
||||
$sReport = "Copies: ".count($aReports).': '.implode('; ', $aReports);
|
||||
} else {
|
||||
$sReport = "No file copy";
|
||||
}
|
||||
return $sReport;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sBackupFileFormat
|
||||
* @param string $sSourceConfigFile
|
||||
* @param string $sMySQLBinDir
|
||||
*
|
||||
* @throws \BackupException
|
||||
* @throws \CoreException
|
||||
* @throws \MySQLException
|
||||
* @since 2.5.0 uses a {@link Config} object to store DB parameters
|
||||
*/
|
||||
protected function DoBackup($sBackupFileFormat, $sSourceConfigFile, $sMySQLBinDir = null)
|
||||
{
|
||||
$oBackup = new SetupDBBackup($this->oConfig);
|
||||
$sTargetFile = $oBackup->MakeName($sBackupFileFormat);
|
||||
if (!empty($sMySQLBinDir)) {
|
||||
$oBackup->SetMySQLBinDir($sMySQLBinDir);
|
||||
}
|
||||
|
||||
CMDBSource::InitFromConfig($this->oConfig);
|
||||
$oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aRemovedExtensionCodes
|
||||
* @param array $aSelectedModules
|
||||
* @param string $sSourceDir
|
||||
* @param string $sExtensionDir
|
||||
* @param boolean $bUseSymbolicLinks
|
||||
*
|
||||
* @return void
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
*
|
||||
* @since 3.1.0 N°2013 added the aParamValues param
|
||||
*/
|
||||
protected function DoCompile($aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, $bUseSymbolicLinks = null)
|
||||
{
|
||||
/**
|
||||
* @since 3.2.0 move the ContextTag init at the very beginning of the method
|
||||
* @noinspection PhpUnusedLocalVariableInspection
|
||||
*/
|
||||
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
|
||||
|
||||
SetupLog::Info("Compiling data model.");
|
||||
|
||||
require_once(APPROOT.'setup/modulediscovery.class.inc.php');
|
||||
require_once(APPROOT.'setup/modelfactory.class.inc.php');
|
||||
require_once(APPROOT.'setup/compiler.class.inc.php');
|
||||
|
||||
$aParamValues = $this->oParams->GetParamForConfigArray();
|
||||
$sEnvironment = $this->GetTargetEnv();
|
||||
$sTargetDir = $this->GetTargetDir();
|
||||
|
||||
if (empty($sSourceDir) || empty($sTargetDir)) {
|
||||
throw new Exception("missing parameter source_dir and/or target_dir");
|
||||
}
|
||||
|
||||
$sSourcePath = APPROOT.$sSourceDir;
|
||||
$aDirsToScan = [$sSourcePath];
|
||||
$sExtensionsPath = APPROOT.$sExtensionDir;
|
||||
if (is_dir($sExtensionsPath)) {
|
||||
// if the extensions dir exists, scan it for additional modules as well
|
||||
$aDirsToScan[] = $sExtensionsPath;
|
||||
}
|
||||
$sExtraPath = APPROOT.'/data/'.$sEnvironment.'-modules/';
|
||||
if (is_dir($sExtraPath)) {
|
||||
// if the extra dir exists, scan it for additional modules as well
|
||||
$aDirsToScan[] = $sExtraPath;
|
||||
}
|
||||
$sTargetPath = APPROOT.$sTargetDir;
|
||||
|
||||
if (!is_dir($sSourcePath)) {
|
||||
throw new Exception("Failed to find the source directory '$sSourcePath', please check the rights of the web server");
|
||||
}
|
||||
|
||||
$bIsAlreadyInMaintenanceMode = SetupUtils::IsInMaintenanceMode();
|
||||
|
||||
if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) {
|
||||
$sConfigFilePath = utils::GetConfigFilePath($sEnvironment);
|
||||
if (is_file($sConfigFilePath)) {
|
||||
$oConfig = new Config($sConfigFilePath);
|
||||
$oConfig->UpdateFromParams($aParamValues);
|
||||
SetupUtils::EnterMaintenanceMode($oConfig);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!is_dir($sTargetPath)) {
|
||||
if (!mkdir($sTargetPath)) {
|
||||
throw new Exception("Failed to create directory '$sTargetPath', please check the rights of the web server");
|
||||
} else {
|
||||
// adjust the rights if and only if the directory was just created
|
||||
// owner:rwx user/group:rx
|
||||
chmod($sTargetPath, 0755);
|
||||
}
|
||||
} elseif (substr($sTargetPath, 0, strlen(APPROOT)) == APPROOT) {
|
||||
// If the directory is under the root folder - as expected - let's clean-it before compiling
|
||||
SetupUtils::tidydir($sTargetPath);
|
||||
}
|
||||
|
||||
$oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan);
|
||||
$oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes);
|
||||
|
||||
$oFactory = new ModelFactory($aDirsToScan);
|
||||
|
||||
$oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries');
|
||||
$oFactory->LoadModule($oDictModule);
|
||||
|
||||
$sDeltaFile = APPROOT.'core/datamodel.core.xml';
|
||||
if (file_exists($sDeltaFile)) {
|
||||
$oCoreModule = new MFCoreModule('core', 'Core Module', $sDeltaFile);
|
||||
$oFactory->LoadModule($oCoreModule);
|
||||
}
|
||||
$sDeltaFile = APPROOT.'application/datamodel.application.xml';
|
||||
if (file_exists($sDeltaFile)) {
|
||||
$oApplicationModule = new MFCoreModule('application', 'Application Module', $sDeltaFile);
|
||||
$oFactory->LoadModule($oApplicationModule);
|
||||
}
|
||||
|
||||
$aModules = $oFactory->FindModules();
|
||||
|
||||
foreach ($aModules as $oModule) {
|
||||
$sModule = $oModule->GetName();
|
||||
if (in_array($sModule, $aSelectedModules)) {
|
||||
$oFactory->LoadModule($oModule);
|
||||
}
|
||||
}
|
||||
// Dump the "reference" model, just before loading any actual delta
|
||||
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$sEnvironment.'.xml');
|
||||
|
||||
$sDeltaFile = utils::GetDataPath().$sEnvironment.'.delta.xml';
|
||||
if (file_exists($sDeltaFile)) {
|
||||
$oDelta = new MFDeltaModule($sDeltaFile);
|
||||
$oFactory->LoadModule($oDelta);
|
||||
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$sEnvironment.'-with-delta.xml');
|
||||
}
|
||||
|
||||
$oMFCompiler = new MFCompiler($oFactory, $sEnvironment);
|
||||
$oMFCompiler->Compile($sTargetPath, null, $bUseSymbolicLinks);
|
||||
//$aCompilerLog = $oMFCompiler->GetLog();
|
||||
//SetupLog::Info(implode("\n", $aCompilerLog));
|
||||
SetupLog::Info("Data model successfully compiled to '$sTargetPath'.");
|
||||
|
||||
$sCacheDir = APPROOT.'/data/cache-'.$sEnvironment.'/';
|
||||
SetupUtils::builddir($sCacheDir);
|
||||
SetupUtils::tidydir($sCacheDir);
|
||||
} catch (Exception $e) {
|
||||
if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) {
|
||||
SetupUtils::ExitMaintenanceMode();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Special case to patch a ugly patch in itop-config-mgmt
|
||||
$sFileToPatch = $sTargetPath.'/itop-config-mgmt-1.0.0/model.itop-config-mgmt.php';
|
||||
if (file_exists($sFileToPatch)) {
|
||||
$sContent = file_get_contents($sFileToPatch);
|
||||
|
||||
$sContent = str_replace("require_once(APPROOT.'modules/itop-welcome-itil/model.itop-welcome-itil.php');", "//\n// The line below is no longer needed in iTop 2.0 -- patched by the setup program\n// require_once(APPROOT.'modules/itop-welcome-itil/model.itop-welcome-itil.php');", $sContent);
|
||||
|
||||
file_put_contents($sFileToPatch, $sContent);
|
||||
}
|
||||
|
||||
// Set an "Instance UUID" identifying this machine based on a file located in the data directory
|
||||
$sInstanceUUIDFile = utils::GetDataPath().'instance.txt';
|
||||
SetupUtils::builddir(utils::GetDataPath());
|
||||
if (!file_exists($sInstanceUUIDFile)) {
|
||||
$sIntanceUUID = utils::CreateUUID('filesystem');
|
||||
file_put_contents($sInstanceUUIDFile, $sIntanceUUID);
|
||||
}
|
||||
if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) {
|
||||
SetupUtils::ExitMaintenanceMode();
|
||||
}
|
||||
}
|
||||
|
||||
protected function GetModelInfoPath(string $sEnv): string
|
||||
{
|
||||
return APPROOT."data/beforecompilation_".$sEnv."_modelinfo.json";
|
||||
}
|
||||
|
||||
protected function SaveModelInfo(string $sEnvironment): bool
|
||||
{
|
||||
$sModelInfoPath = $this->GetModelInfoPath($sEnvironment);
|
||||
try {
|
||||
$aModelInfo = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($sEnvironment);
|
||||
} catch (Exception $e) {
|
||||
//logged already
|
||||
return is_file($sModelInfoPath);
|
||||
}
|
||||
|
||||
return (bool) file_put_contents($sModelInfoPath, json_encode($aModelInfo));
|
||||
}
|
||||
|
||||
protected function GetPreviousModelInfo(string $sEnvironment): array
|
||||
{
|
||||
$sContent = file_get_contents($this->GetModelInfoPath($sEnvironment));
|
||||
$aModelInfo = json_decode($sContent, true);
|
||||
|
||||
if (false === $aModelInfo) {
|
||||
throw new Exception("Could not read (before compilation) previous model to audit data");
|
||||
}
|
||||
|
||||
return $aModelInfo;
|
||||
}
|
||||
|
||||
protected function IsSetupDataAuditEnabled($sSkipDataAudit, array $aParamValues): bool
|
||||
{
|
||||
if ($sSkipDataAudit === "checked") {
|
||||
SetupLog::Info("Setup data audit disabled", null, ['skip-data-audit' => $sSkipDataAudit]);
|
||||
return false;
|
||||
}
|
||||
|
||||
$sMode = $aParamValues['mode'];
|
||||
if ($sMode !== "upgrade") {
|
||||
//first install
|
||||
return false;
|
||||
}
|
||||
|
||||
$sPath = APPROOT.$this->GetTargetDir();
|
||||
if (!is_dir($sPath)) {
|
||||
SetupLog::Info("Reinstallation of an iTop from a backup (No ".$this->GetTargetDir()." found). Setup data audit disabled", null, ['skip-data-audit' => $sSkipDataAudit]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $aSelectedModules
|
||||
*
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
protected function DoUpdateDBSchema($aSelectedModules)
|
||||
{
|
||||
$sTargetEnvironment = $this->GetTargetEnv();
|
||||
$sModulesDir = $this->GetTargetDir();
|
||||
$aParamValues = $this->oParams->GetParamForConfigArray();
|
||||
/**
|
||||
* @since 3.2.0 move the ContextTag init at the very beginning of the method
|
||||
* @noinspection PhpUnusedLocalVariableInspection
|
||||
*/
|
||||
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
|
||||
SetupLog::Info("Update Database Schema for environment '$sTargetEnvironment'.");
|
||||
$sMode = $aParamValues['mode'];
|
||||
$sDBPrefix = $aParamValues['db_prefix'];
|
||||
$sDBName = $aParamValues['db_name'];
|
||||
|
||||
$oConfig = new Config();
|
||||
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
|
||||
|
||||
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
|
||||
$oProductionEnv->InitDataModel($oConfig, true); // load data model only
|
||||
|
||||
// Migrate columns
|
||||
self::MoveColumns($sDBPrefix);
|
||||
|
||||
// Migrate application data format
|
||||
//
|
||||
// priv_internalUser caused troubles because MySQL transforms table names to lower case under Windows
|
||||
// This becomes an issue when moving your installation data to/from Windows
|
||||
// Starting 2.0, all table names must be lowercase
|
||||
if ($sMode != 'install') {
|
||||
SetupLog::Info("Renaming '{$sDBPrefix}priv_internalUser' into '{$sDBPrefix}priv_internaluser' (lowercase)");
|
||||
// This command will have no effect under Windows...
|
||||
// and it has been written in two steps so as to make it work under windows!
|
||||
CMDBSource::SelectDB($sDBName);
|
||||
try {
|
||||
$sRepair = "RENAME TABLE `{$sDBPrefix}priv_internalUser` TO `{$sDBPrefix}priv_internaluser_other`, `{$sDBPrefix}priv_internaluser_other` TO `{$sDBPrefix}priv_internaluser`";
|
||||
CMDBSource::Query($sRepair);
|
||||
} catch (Exception $e) {
|
||||
SetupLog::Info("Renaming '{$sDBPrefix}priv_internalUser' failed (already done in a previous upgrade?)");
|
||||
}
|
||||
|
||||
// let's remove the records in priv_change which have no counterpart in priv_changeop
|
||||
SetupLog::Info("Cleanup of '{$sDBPrefix}priv_change' to remove orphan records");
|
||||
CMDBSource::SelectDB($sDBName);
|
||||
try {
|
||||
$sTotalCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_change`";
|
||||
$iTotalCount = (int)CMDBSource::QueryToScalar($sTotalCount);
|
||||
SetupLog::Info("There is a total of $iTotalCount records in {$sDBPrefix}priv_change.");
|
||||
|
||||
$sOrphanCount = "SELECT COUNT(c.id) FROM `{$sDBPrefix}priv_change` AS c left join `{$sDBPrefix}priv_changeop` AS o ON c.id = o.changeid WHERE o.id IS NULL";
|
||||
$iOrphanCount = (int)CMDBSource::QueryToScalar($sOrphanCount);
|
||||
SetupLog::Info("There are $iOrphanCount useless records in {$sDBPrefix}priv_change (".sprintf('%.2f', ((100.0 * $iOrphanCount) / $iTotalCount))."%)");
|
||||
if ($iOrphanCount > 0) {
|
||||
//N°3793
|
||||
if ($iOrphanCount > 100000) {
|
||||
SetupLog::Info("There are too much useless records ($iOrphanCount) in {$sDBPrefix}priv_change. Cleanup cannot be done during setup.");
|
||||
} else {
|
||||
SetupLog::Info("Removing the orphan records...");
|
||||
$sCleanup = "DELETE FROM `{$sDBPrefix}priv_change` USING `{$sDBPrefix}priv_change` LEFT JOIN `{$sDBPrefix}priv_changeop` ON `{$sDBPrefix}priv_change`.id = `{$sDBPrefix}priv_changeop`.changeid WHERE `{$sDBPrefix}priv_changeop`.id IS NULL;";
|
||||
CMDBSource::Query($sCleanup);
|
||||
SetupLog::Info("Cleanup completed successfully.");
|
||||
}
|
||||
} else {
|
||||
SetupLog::Info("Ok, nothing to cleanup.");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
SetupLog::Info("Cleanup of orphan records in `{$sDBPrefix}priv_change` failed: ".$e->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Module specific actions (migrate the data)
|
||||
$aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir);
|
||||
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation', $aSelectedModules);
|
||||
|
||||
if (!$oProductionEnv->CreateDatabaseStructure(MetaModel::GetConfig(), $sMode)) {
|
||||
throw new Exception("Failed to create/upgrade the database structure for environment '$sTargetEnvironment'");
|
||||
}
|
||||
|
||||
// Set a DBProperty with a unique ID to identify this instance of iTop
|
||||
$sUUID = DBProperty::GetProperty('database_uuid', '');
|
||||
if ($sUUID === '') {
|
||||
$sUUID = utils::CreateUUID('database');
|
||||
DBProperty::SetProperty('database_uuid', $sUUID, 'Installation/upgrade of '.ITOP_APPLICATION, 'Unique ID of this '.ITOP_APPLICATION.' Database');
|
||||
}
|
||||
|
||||
// priv_change now has an 'origin' field to distinguish between the various input sources
|
||||
// Let's initialize the field with 'interactive' for all records were it's null
|
||||
// Then check if some records should hold a different value, based on a pattern matching in the userinfo field
|
||||
CMDBSource::SelectDB($sDBName);
|
||||
try {
|
||||
$sCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_change` WHERE `origin` IS NULL";
|
||||
$iCount = (int)CMDBSource::QueryToScalar($sCount);
|
||||
if ($iCount > 0) {
|
||||
SetupLog::Info("Initializing '{$sDBPrefix}priv_change.origin' ($iCount records to update)");
|
||||
|
||||
// By default all uninitialized values are considered as 'interactive'
|
||||
$sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'interactive' WHERE `origin` IS NULL";
|
||||
CMDBSource::Query($sInit);
|
||||
|
||||
// CSV Import was identified by the comment at the end
|
||||
$sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'csv-import.php' WHERE `userinfo` LIKE '%Web Service (CSV)'";
|
||||
CMDBSource::Query($sInit);
|
||||
|
||||
// CSV Import was identified by the comment at the end
|
||||
$sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'csv-interactive' WHERE `userinfo` LIKE '%(CSV)' AND origin = 'interactive'";
|
||||
CMDBSource::Query($sInit);
|
||||
|
||||
// Syncho data sources were identified by the comment at the end
|
||||
// Unfortunately the comment is localized, so we have to search for all possible patterns
|
||||
$sCurrentLanguage = Dict::GetUserLanguage();
|
||||
$aSuffixes = [];
|
||||
foreach (array_keys(Dict::GetLanguages()) as $sLangCode) {
|
||||
Dict::SetUserLanguage($sLangCode);
|
||||
$sSuffix = CMDBSource::Quote('%'.Dict::S('Core:SyncDataExchangeComment'));
|
||||
$aSuffixes[$sSuffix] = true;
|
||||
}
|
||||
Dict::SetUserLanguage($sCurrentLanguage);
|
||||
$sCondition = "`userinfo` LIKE ".implode(" OR `userinfo` LIKE ", array_keys($aSuffixes));
|
||||
|
||||
$sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'synchro-data-source' WHERE ($sCondition)";
|
||||
CMDBSource::Query($sInit);
|
||||
|
||||
SetupLog::Info("Initialization of '{$sDBPrefix}priv_change.origin' completed.");
|
||||
} else {
|
||||
SetupLog::Info("'{$sDBPrefix}priv_change.origin' already initialized, nothing to do.");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
SetupLog::Error("Initializing '{$sDBPrefix}priv_change.origin' failed: ".$e->getMessage());
|
||||
}
|
||||
|
||||
// priv_async_task now has a 'status' field to distinguish between the various statuses rather than just relying on the date columns
|
||||
// Let's initialize the field with 'planned' or 'error' for all records were it's null
|
||||
CMDBSource::SelectDB($sDBName);
|
||||
try {
|
||||
$sCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_async_task` WHERE `status` IS NULL";
|
||||
$iCount = (int)CMDBSource::QueryToScalar($sCount);
|
||||
if ($iCount > 0) {
|
||||
SetupLog::Info("Initializing '{$sDBPrefix}priv_async_task.status' ($iCount records to update)");
|
||||
|
||||
$sInit = "UPDATE `{$sDBPrefix}priv_async_task` SET `status` = 'planned' WHERE (`status` IS NULL) AND (`started` IS NULL)";
|
||||
CMDBSource::Query($sInit);
|
||||
|
||||
$sInit = "UPDATE `{$sDBPrefix}priv_async_task` SET `status` = 'error' WHERE (`status` IS NULL) AND (`started` IS NOT NULL)";
|
||||
CMDBSource::Query($sInit);
|
||||
|
||||
SetupLog::Info("Initialization of '{$sDBPrefix}priv_async_task.status' completed.");
|
||||
} else {
|
||||
SetupLog::Info("'{$sDBPrefix}priv_async_task.status' already initialized, nothing to do.");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
SetupLog::Error("Initializing '{$sDBPrefix}priv_async_task.status' failed: ".$e->getMessage());
|
||||
}
|
||||
|
||||
SetupLog::Info("Database Schema Successfully Updated for environment '$sTargetEnvironment'.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sDBPrefix
|
||||
*
|
||||
* @throws \CoreException
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
protected static function MoveColumns($sDBPrefix)
|
||||
{
|
||||
// In 2.6.0 the 'fields' attribute has been moved from Query to QueryOQL for dependencies reasons
|
||||
ModuleInstallerAPI::MoveColumnInDB($sDBPrefix.'priv_query', 'fields', $sDBPrefix.'priv_query_oql', 'fields');
|
||||
}
|
||||
|
||||
protected function AfterDBCreate(
|
||||
$sAdminUser,
|
||||
$sAdminPwd,
|
||||
$sAdminLanguage,
|
||||
$aSelectedModules
|
||||
) {
|
||||
$aParamValues = $this->oParams->GetParamForConfigArray();
|
||||
$sTargetEnvironment = $this->GetTargetEnv();
|
||||
$sModulesDir = $this->GetTargetDir();
|
||||
|
||||
/**
|
||||
* @since 3.2.0 move the ContextTag init at the very beginning of the method
|
||||
* @noinspection PhpUnusedLocalVariableInspection
|
||||
*/
|
||||
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
|
||||
SetupLog::Info('After Database Creation');
|
||||
|
||||
$sMode = $aParamValues['mode'];
|
||||
$oConfig = new Config();
|
||||
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
|
||||
|
||||
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
|
||||
$oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database
|
||||
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
|
||||
self::$bMetaModelStarted = true; // No need to reload the final MetaModel in case the installer runs synchronously
|
||||
|
||||
// Perform here additional DB setup... profiles, etc...
|
||||
//
|
||||
$aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir);
|
||||
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation', $aSelectedModules);
|
||||
|
||||
$oProductionEnv->UpdatePredefinedObjects();
|
||||
|
||||
if ($sMode == 'install') {
|
||||
if (!self::CreateAdminAccount(MetaModel::GetConfig(), $sAdminUser, $sAdminPwd, $sAdminLanguage)) {
|
||||
throw(new Exception("Failed to create the administrator account '$sAdminUser'"));
|
||||
} else {
|
||||
SetupLog::Info("Administrator account '$sAdminUser' created.");
|
||||
}
|
||||
}
|
||||
|
||||
// Perform final setup tasks here
|
||||
//
|
||||
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup', $aSelectedModules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create and administrator account for iTop
|
||||
* @return boolean true on success, false otherwise
|
||||
*/
|
||||
protected static function CreateAdminAccount(Config $oConfig, $sAdminUser, $sAdminPwd, $sLanguage)
|
||||
{
|
||||
SetupLog::Info('CreateAdminAccount');
|
||||
|
||||
if (UserRights::CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function DoLoadFiles(
|
||||
$aSelectedModules,
|
||||
$bSampleData = false
|
||||
) {
|
||||
$aParamValues = $this->oParams->GetParamForConfigArray();
|
||||
$sTargetEnvironment = $this->GetTargetEnv();
|
||||
$sModulesDir = $this->GetTargetDir();
|
||||
|
||||
/**
|
||||
* @since 3.2.0 move the ContextTag init at the very beginning of the method
|
||||
* @noinspection PhpUnusedLocalVariableInspection
|
||||
*/
|
||||
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
|
||||
|
||||
$oConfig = new Config();
|
||||
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
|
||||
|
||||
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
|
||||
|
||||
//Load the MetaModel if needed (asynchronous mode)
|
||||
if (!self::$bMetaModelStarted) {
|
||||
$oProductionEnv->InitDataModel($oConfig, false); // load data model and connect to the database
|
||||
|
||||
self::$bMetaModelStarted = true; // No need to reload the final MetaModel in case the installer runs synchronously
|
||||
}
|
||||
|
||||
$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, APPROOT.$sModulesDir);
|
||||
$oProductionEnv->LoadData($aAvailableModules, $bSampleData, $aSelectedModules);
|
||||
|
||||
// Perform after dbload setup tasks here
|
||||
//
|
||||
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad', $aSelectedModules);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sPreviousConfigFile
|
||||
* @param string $sDataModelVersion
|
||||
* @param array $aSelectedModuleCodes
|
||||
* @param array $aSelectedExtensionCodes
|
||||
* @param string|null $sInstallComment
|
||||
*
|
||||
* @param null $sInstallComment
|
||||
*
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function DoCreateConfig(
|
||||
$sPreviousConfigFile,
|
||||
$sDataModelVersion,
|
||||
$aSelectedModuleCodes,
|
||||
$aSelectedExtensionCodes,
|
||||
$sInstallComment = null
|
||||
) {
|
||||
$aParamValues = $this->oParams->GetParamForConfigArray();
|
||||
$sTargetEnvironment = $this->GetTargetEnv();
|
||||
$sModulesDir = $this->GetTargetDir();
|
||||
|
||||
/**
|
||||
* @since 3.2.0 move the ContextTag init at the very beginning of the method
|
||||
* @noinspection PhpUnusedLocalVariableInspection
|
||||
*/
|
||||
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
|
||||
|
||||
$aParamValues['selected_modules'] = implode(',', $aSelectedModuleCodes);
|
||||
$sMode = $aParamValues['mode'];
|
||||
|
||||
if ($sMode == 'upgrade') {
|
||||
try {
|
||||
$oOldConfig = new Config($sPreviousConfigFile);
|
||||
$oConfig = clone($oOldConfig);
|
||||
} catch (Exception $e) {
|
||||
// In case the previous configuration is corrupted... start with a blank new one
|
||||
$oConfig = new Config();
|
||||
}
|
||||
} else {
|
||||
$oConfig = new Config();
|
||||
// To preserve backward compatibility while upgrading to 2.0.3 (when tracking_level_linked_set_default has been introduced)
|
||||
// the default value on upgrade differs from the default value at first install
|
||||
$oConfig->Set('tracking_level_linked_set_default', LINKSET_TRACKING_NONE, 'first_install');
|
||||
}
|
||||
|
||||
$oConfig->Set('access_mode', ACCESS_FULL);
|
||||
// Final config update: add the modules
|
||||
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
|
||||
|
||||
// Record which modules are installed...
|
||||
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
|
||||
$oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database
|
||||
|
||||
if (!$oProductionEnv->RecordInstallation($oConfig, $sDataModelVersion, $aSelectedModuleCodes, $aSelectedExtensionCodes, $sInstallComment)) {
|
||||
throw new Exception("Failed to record the installation information");
|
||||
}
|
||||
|
||||
// Make sure the root configuration directory exists
|
||||
if (!file_exists(APPCONF)) {
|
||||
mkdir(APPCONF);
|
||||
chmod(APPCONF, 0770); // RWX for owner and group, nothing for others
|
||||
SetupLog::Info("Created configuration directory: ".APPCONF);
|
||||
}
|
||||
|
||||
// Write the final configuration file
|
||||
$sConfigFile = APPCONF.(($sTargetEnvironment == '') ? 'production' : $sTargetEnvironment).'/'.ITOP_CONFIG_FILE;
|
||||
$sConfigDir = dirname($sConfigFile);
|
||||
@mkdir($sConfigDir);
|
||||
@chmod($sConfigDir, 0770); // RWX for owner and group, nothing for others
|
||||
|
||||
$oConfig->WriteToFile($sConfigFile);
|
||||
|
||||
// try to make the final config file read-only
|
||||
@chmod($sConfigFile, 0440); // Read-only for owner and group, nothing for others
|
||||
|
||||
// Ready to go !!
|
||||
require_once(APPROOT.'core/dict.class.inc.php');
|
||||
MetaModel::ResetAllCaches();
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2013-2026 Combodo SAS
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
require_once(APPROOT.'setup/parameters.class.inc.php');
|
||||
require_once(APPROOT.'setup/xmldataloader.class.inc.php');
|
||||
require_once APPROOT.'setup/feature_removal/SetupAudit.php';
|
||||
|
||||
require_once(APPROOT.'setup/sequencers/StepSequencer.php');
|
||||
require_once(APPROOT.'setup/sequencers/ApplicationInstallSequencer.php');
|
||||
|
||||
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
|
||||
|
||||
class DataAuditSequencer extends ApplicationInstallSequencer
|
||||
{
|
||||
public const DATA_AUDIT_FAILED = 100;
|
||||
|
||||
protected function GetTempEnv()
|
||||
{
|
||||
$sTargetEnv = $this->GetTargetEnv();
|
||||
return 'dry-'.$sTargetEnv;
|
||||
}
|
||||
|
||||
protected function GetTargetDir()
|
||||
{
|
||||
$sTargetEnv = $this->GetTempEnv();
|
||||
return 'env-'.$sTargetEnv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the next step of the installation and reports about the progress
|
||||
* and the next step to perform
|
||||
*
|
||||
* @param string $sStep The identifier of the step to execute
|
||||
* @param string|null $sInstallComment
|
||||
*
|
||||
* @return array (status => , message => , percentage-completed => , next-step => , next-step-label => )
|
||||
*/
|
||||
public function ExecuteStep($sStep = '', $sInstallComment = null)
|
||||
{
|
||||
try {
|
||||
$fStart = microtime(true);
|
||||
SetupLog::Info("##### STEP {$sStep} start");
|
||||
$this->EnterReadOnlyMode();
|
||||
switch ($sStep) {
|
||||
case '':
|
||||
$aResult = [
|
||||
'status' => self::OK,
|
||||
'message' => '',
|
||||
'percentage-completed' => 20,
|
||||
'next-step' => 'compile',
|
||||
'next-step-label' => 'Compiling the data model',
|
||||
];
|
||||
|
||||
// Log the parameters...
|
||||
$oDoc = new DOMDocument('1.0', 'UTF-8');
|
||||
$oDoc->preserveWhiteSpace = false;
|
||||
$oDoc->formatOutput = true;
|
||||
$this->oParams->ToXML($oDoc, null, 'installation');
|
||||
$sXML = $oDoc->saveXML();
|
||||
$sSafeXml = preg_replace("|<pwd>([^<]*)</pwd>|", "<pwd>**removed**</pwd>", $sXML);
|
||||
SetupLog::Info("======= Data Audit starts =======\nParameters:\n$sSafeXml\n");
|
||||
|
||||
// Save the response file as a stand-alone file as well
|
||||
$sFileName = 'data-audit-'.date('Y-m-d');
|
||||
$index = 0;
|
||||
while (file_exists(APPROOT.'log/'.$sFileName.'.xml')) {
|
||||
$index++;
|
||||
$sFileName = 'data-audit-'.date('Y-m-d').'-'.$index;
|
||||
}
|
||||
file_put_contents(APPROOT.'log/'.$sFileName.'.xml', $sSafeXml);
|
||||
|
||||
break;
|
||||
|
||||
case 'compile':
|
||||
$aSelectedModules = $this->oParams->Get('selected_modules');
|
||||
$sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest');
|
||||
$sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions');
|
||||
$aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', []);
|
||||
|
||||
$this->DoCompile(
|
||||
$aRemovedExtensionCodes,
|
||||
$aSelectedModules,
|
||||
$sSourceDir,
|
||||
$sExtensionDir,
|
||||
false
|
||||
);
|
||||
|
||||
$aResult = [
|
||||
'status' => self::OK,
|
||||
'message' => '',
|
||||
'next-step' => 'write-config',
|
||||
'next-step-label' => 'Writing audit config',
|
||||
'percentage-completed' => 40,
|
||||
];
|
||||
break;
|
||||
case 'write-config':
|
||||
$this->DoWriteConfig();
|
||||
$aResult = [
|
||||
'status' => self::OK,
|
||||
'message' => '',
|
||||
'next-step' => 'setup-audit',
|
||||
'next-step-label' => 'Checking data consistency with the new data model',
|
||||
'percentage-completed' => 60,
|
||||
];
|
||||
break;
|
||||
case 'setup-audit':
|
||||
$this->DoSetupAudit();
|
||||
$aResult = [
|
||||
'status' => self::OK,
|
||||
'message' => '',
|
||||
'next-step' => 'cleanup',
|
||||
'next-step-label' => 'Temporary folders cleanup',
|
||||
'percentage-completed' => 80,
|
||||
];
|
||||
break;
|
||||
case 'cleanup' :
|
||||
$this->DoCleanup();
|
||||
$aResult = [
|
||||
'status' => self::OK,
|
||||
'message' => '',
|
||||
'next-step' => '',
|
||||
'next-step-label' => 'Completed',
|
||||
'percentage-completed' => 100,
|
||||
];
|
||||
break;
|
||||
default:
|
||||
$aResult = [
|
||||
'status' => self::ERROR,
|
||||
'message' => '',
|
||||
'next-step' => '',
|
||||
'next-step-label' => "Unknown setup step '$sStep'.",
|
||||
'percentage-completed' => 100,
|
||||
];
|
||||
break;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$aResult = [
|
||||
'status' => self::ERROR,
|
||||
'message' => $e->getMessage(),
|
||||
'next-step' => '',
|
||||
'next-step-label' => '',
|
||||
'percentage-completed' => 100,
|
||||
'error_code' => $e->getCode(),
|
||||
];
|
||||
|
||||
SetupLog::Error('An exception occurred: '.$e->getMessage().' at line '.$e->getLine().' in file '.$e->getFile());
|
||||
$idx = 0;
|
||||
// Log the call stack, but not the parameters since they may contain passwords or other sensitive data
|
||||
SetupLog::Ok("Call stack:");
|
||||
foreach ($e->getTrace() as $aTrace) {
|
||||
$sLine = empty($aTrace['line']) ? "" : $aTrace['line'];
|
||||
$sFile = empty($aTrace['file']) ? "" : $aTrace['file'];
|
||||
$sClass = empty($aTrace['class']) ? "" : $aTrace['class'];
|
||||
$sType = empty($aTrace['type']) ? "" : $aTrace['type'];
|
||||
$sFunction = empty($aTrace['function']) ? "" : $aTrace['function'];
|
||||
$sVerb = empty($sClass) ? $sFunction : "$sClass{$sType}$sFunction";
|
||||
SetupLog::Ok("#$idx $sFile($sLine): $sVerb(...)");
|
||||
$idx++;
|
||||
}
|
||||
$this->ExitReadOnlyMode();
|
||||
} finally {
|
||||
$fDuration = round(microtime(true) - $fStart, 2);
|
||||
SetupLog::Info("##### STEP {$sStep} duration: {$fDuration}s");
|
||||
}
|
||||
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
protected function DoWriteConfig()
|
||||
{
|
||||
$sConfigFilePath = utils::GetConfigFilePath($this->GetTargetEnv());
|
||||
if (is_file($sConfigFilePath)) {
|
||||
$oConfig = new Config($sConfigFilePath);
|
||||
|
||||
$sTempConfigFileName = utils::GetConfigFilePath($this->GetTempEnv());
|
||||
$sConfigDir = dirname($sTempConfigFileName);
|
||||
@mkdir($sConfigDir);
|
||||
@chmod($sConfigDir, 0770); // RWX for owner and group, nothing for others
|
||||
|
||||
return $oConfig->WriteToFile($sTempConfigFileName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function DoSetupAudit()
|
||||
{
|
||||
/**
|
||||
* @since 3.2.0 move the ContextTag init at the very beginning of the method
|
||||
* @noinspection PhpUnusedLocalVariableInspection
|
||||
*/
|
||||
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
|
||||
|
||||
$sTargetEnvironment = $this->GetTempEnv();
|
||||
$sPreviousEnvironment = $this->GetTargetEnv();
|
||||
|
||||
$oSetupAudit = new SetupAudit($sPreviousEnvironment, $sTargetEnvironment);
|
||||
|
||||
//Make sure the MetaModel is started before analysing for issues
|
||||
$sConfFile = utils::GetConfigFilePath($sPreviousEnvironment);
|
||||
MetaModel::Startup($sConfFile, false /* $bModelOnly */, false /* $bAllowCache */, false /* $bTraceSourceFiles */, $sPreviousEnvironment);
|
||||
$oSetupAudit->GetIssues(true);
|
||||
$iCount = $oSetupAudit->GetDataToCleanupCount();
|
||||
|
||||
if ($iCount > 0) {
|
||||
throw new Exception("$iCount elements require data adjustments or cleanup in the backoffice prior to upgrading iTop", static::DATA_AUDIT_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
protected function DoCleanup()
|
||||
{
|
||||
$sDestination = APPROOT.$this->GetTargetDir();
|
||||
SetupUtils::tidydir($sDestination);
|
||||
SetupUtils::rmdir_safe($sDestination);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2013-2026 Combodo SAS
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
abstract class StepSequencer
|
||||
{
|
||||
public const OK = 1;
|
||||
public const ERROR = 2;
|
||||
public const WARNING = 3;
|
||||
public const INFO = 4;
|
||||
|
||||
/**
|
||||
* Runs all the installation steps in one go and directly outputs
|
||||
* some information about the progress and the success of the various
|
||||
* sequential steps.
|
||||
*
|
||||
* @param bool $bVerbose
|
||||
* @param string|null $sMessage
|
||||
* @param string|null $sComment
|
||||
*
|
||||
* @return boolean True if the installation was successful, false otherwise
|
||||
*/
|
||||
public function ExecuteAllSteps($bVerbose = true, &$sMessage = null, $sComment = null)
|
||||
{
|
||||
$sStep = '';
|
||||
$sStepLabel = '';
|
||||
$iOverallStatus = self::OK;
|
||||
do {
|
||||
if ($bVerbose) {
|
||||
if ($sStep != '') {
|
||||
echo "$sStepLabel\n";
|
||||
echo "Executing '$sStep'\n";
|
||||
} else {
|
||||
echo "Starting...\n";
|
||||
}
|
||||
}
|
||||
$aRes = $this->ExecuteStep($sStep, $sComment);
|
||||
$sStep = $aRes['next-step'];
|
||||
$sStepLabel = $aRes['next-step-label'];
|
||||
$sMessage = $aRes['message'];
|
||||
if ($bVerbose) {
|
||||
switch ($aRes['status']) {
|
||||
case self::OK:
|
||||
echo "Ok. ".$aRes['percentage-completed']." % done.\n";
|
||||
break;
|
||||
|
||||
case self::ERROR:
|
||||
$iOverallStatus = self::ERROR;
|
||||
echo "Error: ".$aRes['message']."\n";
|
||||
break;
|
||||
|
||||
case self::WARNING:
|
||||
$iOverallStatus = self::WARNING;
|
||||
echo "Warning: ".$aRes['message']."\n";
|
||||
echo $aRes['percentage-completed']." % done.\n";
|
||||
break;
|
||||
|
||||
case self::INFO:
|
||||
echo "Info: ".$aRes['message']."\n";
|
||||
echo $aRes['percentage-completed']." % done.\n";
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch ($aRes['status']) {
|
||||
case self::ERROR:
|
||||
$iOverallStatus = self::ERROR;
|
||||
break;
|
||||
case self::WARNING:
|
||||
$iOverallStatus = self::WARNING;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (($aRes['status'] != self::ERROR) && ($aRes['next-step'] != ''));
|
||||
|
||||
return ($iOverallStatus == self::OK);
|
||||
}
|
||||
|
||||
abstract public function ExecuteStep($sStep = '', $sComment = null);
|
||||
}
|
||||
@@ -25,17 +25,21 @@ function WizardAsyncAction(sActionCode, oParams, OnErrorFunction)
|
||||
|
||||
function WizardUpdateButtons()
|
||||
{
|
||||
if (CanMoveForward()) {
|
||||
if (CanMoveForward())
|
||||
{
|
||||
$("#btn_next").prop('disabled', false);
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
$("#btn_next").prop('disabled', true);
|
||||
}
|
||||
|
||||
if (CanMoveBackward()) {
|
||||
if (CanMoveBackward())
|
||||
{
|
||||
$("#btn_back").prop('disabled', false);
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
$("#btn_back").prop('disabled', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,23 +254,32 @@ class SetupUtils
|
||||
|
||||
if (!utils::IsModeCLI()) {
|
||||
$sUploadTmpDir = self::GetUploadTmpDir();
|
||||
// check that the upload directory is indeed writable from PHP
|
||||
if (!file_exists($sUploadTmpDir)) {
|
||||
if (empty($sUploadTmpDir)) {
|
||||
$sUploadTmpDir = '/tmp';
|
||||
$aResult[] = new CheckResult(
|
||||
CheckResult::ERROR,
|
||||
"Temporary directory for files upload ($sUploadTmpDir) does not exist or cannot be read by PHP."
|
||||
CheckResult::WARNING,
|
||||
"Temporary directory for files upload is not defined (upload_tmp_dir), assuming that $sUploadTmpDir is used."
|
||||
);
|
||||
} else {
|
||||
if (!is_writable($sUploadTmpDir)) {
|
||||
}
|
||||
// check that the upload directory is indeed writable from PHP
|
||||
if (!empty($sUploadTmpDir)) {
|
||||
if (!file_exists($sUploadTmpDir)) {
|
||||
$aResult[] = new CheckResult(
|
||||
CheckResult::ERROR,
|
||||
"Temporary directory for files upload ($sUploadTmpDir) is not writable."
|
||||
"Temporary directory for files upload ($sUploadTmpDir) does not exist or cannot be read by PHP."
|
||||
);
|
||||
} else {
|
||||
$aResult[] = new CheckResult(
|
||||
CheckResult::TRACE,
|
||||
"Info - Temporary directory for files upload ($sUploadTmpDir) is writable."
|
||||
);
|
||||
if (!is_writable($sUploadTmpDir)) {
|
||||
$aResult[] = new CheckResult(
|
||||
CheckResult::ERROR,
|
||||
"Temporary directory for files upload ($sUploadTmpDir) is not writable."
|
||||
);
|
||||
} else {
|
||||
$aResult[] = new CheckResult(
|
||||
CheckResult::TRACE,
|
||||
"Info - Temporary directory for files upload ($sUploadTmpDir) is writable."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -509,7 +518,7 @@ class SetupUtils
|
||||
}
|
||||
require_once(APPROOT.'setup/modulediscovery.class.inc.php');
|
||||
try {
|
||||
ModuleDiscovery::GetModulesOrderedByDependencies($aDirsToScan, true, $aSelectedModules);
|
||||
ModuleDiscovery::GetAvailableModules($aDirsToScan, true, $aSelectedModules);
|
||||
} catch (Exception $e) {
|
||||
$aResult[] = new CheckResult(CheckResult::ERROR, $e->getMessage());
|
||||
}
|
||||
@@ -590,7 +599,7 @@ class SetupUtils
|
||||
// create and test destination location
|
||||
//
|
||||
$sDestDir = dirname($sDBBackupPath);
|
||||
SetupUtils::builddir($sDestDir);
|
||||
setuputils::builddir($sDestDir);
|
||||
if (!is_dir($sDestDir)) {
|
||||
$aResult[] = new CheckResult(CheckResult::ERROR, "$sDestDir does not exist and could not be created.");
|
||||
}
|
||||
@@ -1546,8 +1555,17 @@ JS
|
||||
return $sHtml;
|
||||
}
|
||||
|
||||
public static function GetConfig(WizardController $oWizard)
|
||||
/**
|
||||
* @param \WizardController $oWizard
|
||||
* @param bool $bAbortOnMissingDependency ...
|
||||
* @param array $aModulesToLoad List of modules to search for, defaults to all if ommitted
|
||||
*
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function AnalyzeInstallation($oWizard, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
|
||||
{
|
||||
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
|
||||
$oConfig = new Config();
|
||||
$sSourceDir = $oWizard->GetParameter('source_dir', '');
|
||||
|
||||
@@ -1561,26 +1579,8 @@ JS
|
||||
|
||||
$aParamValues = $oWizard->GetParamForConfigArray();
|
||||
$aParamValues['source_dir'] = $sRelativeSourceDir;
|
||||
$oConfig->UpdateFromParams($aParamValues);
|
||||
|
||||
return $oConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \WizardController $oWizard
|
||||
* @param bool $bAbortOnMissingDependency ...
|
||||
* @param array $aModulesToLoad List of modules to search for, defaults to all if ommitted
|
||||
*
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function AnalyzeInstallation($oWizard, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
|
||||
{
|
||||
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
|
||||
|
||||
$oConfig = self::GetConfig($oWizard);
|
||||
|
||||
$aDirsToScan = [$oWizard->GetParameter('source_dir', '')];
|
||||
$oConfig->UpdateFromParams($aParamValues, null);
|
||||
$aDirsToScan = [$sSourceDir];
|
||||
|
||||
if (is_dir(APPROOT.'extensions')) {
|
||||
$aDirsToScan[] = APPROOT.'extensions';
|
||||
@@ -1593,10 +1593,6 @@ JS
|
||||
$aDirsToScan[] = $sExtraDir;
|
||||
}
|
||||
$oProductionEnv = new RunTimeEnvironment();
|
||||
$aRemovedExtensionCodes = json_decode($oWizard->GetParameter('removed_extensions'), true) ?? [];
|
||||
$oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan);
|
||||
$oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes);
|
||||
|
||||
$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, $aDirsToScan, $bAbortOnMissingDependency, $aModulesToLoad);
|
||||
|
||||
foreach ($aAvailableModules as $key => $aModule) {
|
||||
@@ -1622,7 +1618,7 @@ JS
|
||||
|
||||
$aParamValues = $oWizard->GetParamForConfigArray();
|
||||
$aParamValues['source_dir'] = '';
|
||||
$oConfig->UpdateFromParams($aParamValues);
|
||||
$oConfig->UpdateFromParams($aParamValues, null);
|
||||
|
||||
$oProductionEnv = new RunTimeEnvironment();
|
||||
return $oProductionEnv->GetApplicationVersion($oConfig);
|
||||
|
||||
@@ -6,6 +6,7 @@ use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
|
||||
require_once(APPROOT.'/application/utils.inc.php');
|
||||
require_once(APPROOT.'/setup/setuppage.class.inc.php');
|
||||
require_once(APPROOT.'/setup/wizardcontroller.class.inc.php');
|
||||
require_once(APPROOT.'/setup/wizardsteps.class.inc.php');
|
||||
|
||||
class InstallationFileService
|
||||
{
|
||||
@@ -70,15 +71,15 @@ class InstallationFileService
|
||||
return $this->aAfterComputationSelectedExtensions;
|
||||
}
|
||||
|
||||
public function SetItopExtensionsMap(iTopExtensionsMap $oItopExtensionsMap): void
|
||||
public function SetItopExtensionsMap(ItopExtensionsMap $oItopExtensionsMap): void
|
||||
{
|
||||
$this->oItopExtensionsMap = $oItopExtensionsMap;
|
||||
}
|
||||
|
||||
public function GetItopExtensionsMap(): iTopExtensionsMap
|
||||
public function GetItopExtensionsMap(): ItopExtensionsMap
|
||||
{
|
||||
if (is_null($this->oItopExtensionsMap)) {
|
||||
$this->oItopExtensionsMap = new iTopExtensionsMap($this->sTargetEnvironment);
|
||||
$this->oItopExtensionsMap = new iTopExtensionsMap($this->sTargetEnvironment, true);
|
||||
}
|
||||
return $this->oItopExtensionsMap;
|
||||
}
|
||||
@@ -258,7 +259,7 @@ class InstallationFileService
|
||||
{
|
||||
$sProductionModuleDir = APPROOT.'data/'.$this->sTargetEnvironment.'-modules/';
|
||||
|
||||
$aAvailableModules = $this->GetProductionEnv()->AnalyzeInstallation(MetaModel::GetConfig(), $this->GetExtraDirs());
|
||||
$aAvailableModules = $this->GetProductionEnv()->AnalyzeInstallation(MetaModel::GetConfig(), $this->GetExtraDirs(), false, null);
|
||||
|
||||
$this->aAutoSelectModules = [];
|
||||
foreach ($aAvailableModules as $sModuleId => $aModule) {
|
||||
|
||||
@@ -272,7 +272,7 @@ $bFoundIssues = false;
|
||||
$bInstall = utils::ReadParam('install', true, true /* CLI allowed */);
|
||||
if ($bInstall) {
|
||||
echo "Starting the unattended installation...\n";
|
||||
$oWizard = new ApplicationInstallSequencer($oParams);
|
||||
$oWizard = new ApplicationInstaller($oParams);
|
||||
$bRes = $oWizard->ExecuteAllSteps();
|
||||
if (!$bRes) {
|
||||
echo "\nencountered installation issues!";
|
||||
|
||||
@@ -32,6 +32,7 @@ require_once(APPROOT.'/application/utils.inc.php');
|
||||
require_once(APPROOT.'/core/config.class.inc.php');
|
||||
require_once(APPROOT.'/setup/setuppage.class.inc.php');
|
||||
require_once(APPROOT.'/setup/wizardcontroller.class.inc.php');
|
||||
require_once(APPROOT.'/setup/wizardsteps.class.inc.php');
|
||||
|
||||
Session::Start();
|
||||
clearstatcache(); // Make sure we know what we are doing !
|
||||
|
||||
@@ -16,32 +16,9 @@
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
require_once(APPROOT.'setup/setuputils.class.inc.php');
|
||||
require_once(APPROOT.'setup/parameters.class.inc.php');
|
||||
require_once(APPROOT.'setup/applicationinstaller.class.inc.php');
|
||||
require_once(APPROOT.'core/mutex.class.inc.php');
|
||||
require_once(APPROOT.'setup/extensionsmap.class.inc.php');
|
||||
|
||||
require_once(APPROOT.'setup/wizardsteps/WizardStep.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/AbstractWizStepInstall.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepWelcome.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepInstallOrUpgrade.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepDetectedInfo.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepLicense.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepLicense2.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/AbstractWizStepMiscParams.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepAdminAccount.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepInstall.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepDataAudit.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepDBParams.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepDone.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepInstallMiscParams.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepModulesChoice.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepSummary.php');
|
||||
require_once(APPROOT.'setup/wizardsteps/WizStepUpgradeMiscParams.php');
|
||||
|
||||
/**
|
||||
* Engine for displaying the various pages of a "wizard"
|
||||
* Each "step" of the wizard must be implemented as
|
||||
@@ -77,7 +54,7 @@ class WizardController
|
||||
|
||||
/**
|
||||
* Pushes information about the current step onto the stack
|
||||
* @param array $aStepInfo Array('class' => , 'state' => )
|
||||
* @param hash $aStepInfo Array('class' => , 'state' => )
|
||||
*/
|
||||
protected function PushStep($aStepInfo)
|
||||
{
|
||||
@@ -157,7 +134,7 @@ class WizardController
|
||||
public function Start()
|
||||
{
|
||||
$sCurrentStepClass = $this->sInitialStepClass;
|
||||
$oStep = $this->NewStep($sCurrentStepClass, $this->sInitialState);
|
||||
$oStep = new $sCurrentStepClass($this, $this->sInitialState);
|
||||
$this->DisplayStep($oStep);
|
||||
}
|
||||
/**
|
||||
@@ -169,15 +146,13 @@ class WizardController
|
||||
$sCurrentStepClass = utils::ReadParam('_class', $this->sInitialStepClass);
|
||||
$sCurrentState = utils::ReadParam('_state', $this->sInitialState);
|
||||
/** @var \WizardStep $oStep */
|
||||
$oStep = $oStep = $this->NewStep($sCurrentStepClass, $sCurrentState);
|
||||
$oStep = new $sCurrentStepClass($this, $sCurrentState);
|
||||
if ($oStep->ValidateParams()) {
|
||||
if ($oStep->CanComeBack()) {
|
||||
$this->PushStep(['class' => $sCurrentStepClass, 'state' => $sCurrentState]);
|
||||
}
|
||||
$this->PushStep(['class' => $sCurrentStepClass, 'state' => $sCurrentState]);
|
||||
$aPossibleSteps = $oStep->GetPossibleSteps();
|
||||
$aNextStepInfo = $oStep->UpdateWizardStateAndGetNextStep(true); // true => moving forward
|
||||
$aNextStepInfo = $oStep->ProcessParams(true); // true => moving forward
|
||||
if (in_array($aNextStepInfo['class'], $aPossibleSteps)) {
|
||||
$oNextStep = $this->NewStep($aNextStepInfo['class'], $aNextStepInfo['state']);
|
||||
$oNextStep = new $aNextStepInfo['class']($this, $aNextStepInfo['state']);
|
||||
$this->DisplayStep($oNextStep);
|
||||
} else {
|
||||
throw new Exception("Internal error: Unexpected next step '{$aNextStepInfo['class']}'. The possible next steps are: ".implode(', ', $aPossibleSteps));
|
||||
@@ -194,12 +169,12 @@ class WizardController
|
||||
// let the current step save its parameters
|
||||
$sCurrentStepClass = utils::ReadParam('_class', $this->sInitialStepClass);
|
||||
$sCurrentState = utils::ReadParam('_state', $this->sInitialState);
|
||||
$oStep = $this->NewStep($sCurrentStepClass, $sCurrentState);
|
||||
$aNextStepInfo = $oStep->UpdateWizardStateAndGetNextStep(false); // false => Moving backwards
|
||||
$oStep = new $sCurrentStepClass($this, $sCurrentState);
|
||||
$aNextStepInfo = $oStep->ProcessParams(false); // false => Moving backwards
|
||||
|
||||
// Display the previous step
|
||||
$aCurrentStepInfo = $this->PopStep();
|
||||
$oStep = $this->NewStep($aCurrentStepInfo['class'], $aCurrentStepInfo['state']);
|
||||
$oStep = new $aCurrentStepInfo['class']($this, $aCurrentStepInfo['state']);
|
||||
$this->DisplayStep($oStep);
|
||||
}
|
||||
|
||||
@@ -235,12 +210,12 @@ HTML;
|
||||
}
|
||||
}
|
||||
$oPage->LinkScriptFromAppRoot('setup/setup.js');
|
||||
$oPage->add_script("function CanMoveForward()\n{\n".$oStep->JSCanMoveForward()."\n}\n");
|
||||
$oPage->add_script("function CanMoveBackward()\n{\n".$oStep->JSCanMoveBackward()."\n}\n");
|
||||
$oPage->add('<form id="wiz_form" class="ibo-setup--wizard" method="post">');
|
||||
$oPage->add('<div class="ibo-setup--wizard--content">');
|
||||
$oStep->Display($oPage);
|
||||
$oPage->add('</div>');
|
||||
$oPage->add_script("function CanMoveForward()\n{\n".$oStep->JSCanMoveForward()."\n}\n");
|
||||
$oPage->add_script("function CanMoveBackward()\n{\n".$oStep->JSCanMoveBackward()."\n}\n");
|
||||
|
||||
// Add the back / next buttons and the hidden form
|
||||
// to store the parameters
|
||||
@@ -341,7 +316,7 @@ on the page's parameters
|
||||
$sStep = $this->sInitialStepClass;
|
||||
}
|
||||
|
||||
$oStep = $this->NewStep($sStep);
|
||||
$oStep = new $sStep($this, '');
|
||||
$aAllSteps[$sStep] = $oStep->GetPossibleSteps();
|
||||
foreach ($aAllSteps[$sStep] as $sNextStep) {
|
||||
if (!array_key_exists($sNextStep, $aAllSteps)) {
|
||||
@@ -373,7 +348,7 @@ on the page's parameters
|
||||
$sOutput .= "\tnode [shape = doublecircle]; ".implode(' ', $aDeadEnds).";\n";
|
||||
$sOutput .= "\tnode [shape = box];\n";
|
||||
foreach ($aAllSteps as $sStep => $aNextSteps) {
|
||||
$oStep = $this->NewStep($sStep);
|
||||
$oStep = new $sStep($this, '');
|
||||
$sOutput .= "\t$sStep [ label = \"".$oStep->GetTitle()."\"];\n";
|
||||
if (count($aNextSteps) > 0) {
|
||||
foreach ($aNextSteps as $sNextStep) {
|
||||
@@ -384,19 +359,326 @@ on the page's parameters
|
||||
$sOutput .= "}\n";
|
||||
return $sOutput;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class to build "steps" for the wizard controller
|
||||
* If a step needs to maintain an internal "state" (for complex steps)
|
||||
* then it's up to the derived class to implement the behavior based on
|
||||
* the internal 'sCurrentState' variable.
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
abstract class WizardStep
|
||||
{
|
||||
/**
|
||||
* A reference to the WizardController
|
||||
* @var WizardController
|
||||
*/
|
||||
protected $oWizard;
|
||||
/**
|
||||
* Current 'state' of the wizard step. Simple 'steps' can ignore it
|
||||
* @var string
|
||||
*/
|
||||
protected $sCurrentState;
|
||||
|
||||
public function __construct(WizardController $oWizard, $sCurrentState)
|
||||
{
|
||||
$this->oWizard = $oWizard;
|
||||
$this->sCurrentState = $sCurrentState;
|
||||
}
|
||||
|
||||
public function GetState()
|
||||
{
|
||||
return $this->sCurrentState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $sCurrentStepClass
|
||||
* @param $sCurrentState
|
||||
*
|
||||
* @return \WizardStep
|
||||
* @throws \Exception
|
||||
* Displays the wizard page for the current class/state
|
||||
* The page can contain any number of "<input/>" fields, but no "<form>...</form>" tag
|
||||
* The name of the input fields (and their id if one is supplied) MUST NOT start with "_"
|
||||
* (this is reserved for the wizard's own parameters)
|
||||
* @return void
|
||||
*/
|
||||
private function NewStep($sCurrentStepClass, $sCurrentState = '')
|
||||
abstract public function Display(WebPage $oPage);
|
||||
/**
|
||||
* Displays the wizard page for the current class/state
|
||||
* return UIBlock
|
||||
* The name of the input fields (and their id if one is supplied) MUST NOT start with "_"
|
||||
* (this is reserved for the wizard's own parameters)
|
||||
* @return \Combodo\iTop\Application\UI\Base\UIBlock
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public function DisplayBlock(WebPage $oPage)
|
||||
{
|
||||
return new Html($this->Display($oPage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the page's parameters and (if moving forward) returns the next step/state to be displayed
|
||||
* @param bool $bMoveForward True if the wizard is moving forward 'Next >>' button pressed, false otherwise
|
||||
* @return hash array('class' => $sNextClass, 'state' => $sNextState)
|
||||
*/
|
||||
abstract public function ProcessParams($bMoveForward = true);
|
||||
|
||||
/**
|
||||
* Returns the list of possible steps from this step forward
|
||||
* @return array Array of strings (step classes)
|
||||
*/
|
||||
abstract public function GetPossibleSteps();
|
||||
|
||||
/**
|
||||
* Returns title of the current step
|
||||
* @return string The title of the wizard page for the current step
|
||||
*/
|
||||
abstract public function GetTitle();
|
||||
|
||||
/**
|
||||
* Tells whether the parameters are Ok to move forward
|
||||
* @return boolean True to move forward, false to stey on the same step
|
||||
*/
|
||||
public function ValidateParams()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether this step/state is the last one of the wizard (dead-end)
|
||||
* @return boolean True if the 'Next >>' button should be displayed
|
||||
*/
|
||||
public function CanMoveForward()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the "Next" button should be enabled interactively
|
||||
* @return string A piece of javascript code returning either true or false
|
||||
*/
|
||||
public function JSCanMoveForward()
|
||||
{
|
||||
return 'return true;';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the label for the " Next >> " button
|
||||
* @return string The label for the button
|
||||
*/
|
||||
public function GetNextButtonLabel()
|
||||
{
|
||||
return 'Next';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether this step/state allows to go back or not
|
||||
* @return boolean True if the '<< Back' button should be displayed
|
||||
*/
|
||||
public function CanMoveBackward()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the "Back" button should be enabled interactively
|
||||
* @return string A piece of javascript code returning either true or false
|
||||
*/
|
||||
public function JSCanMoveBackward()
|
||||
{
|
||||
return 'return true;';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether this step of the wizard requires that the configuration file be writable
|
||||
* @return bool True if the wizard will possibly need to modify the configuration at some point
|
||||
*/
|
||||
public function RequiresWritableConfig()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload this function to implement asynchronous action(s) (AJAX)
|
||||
* @param string $sCode The code of the action (if several actions need to be distinguished)
|
||||
* @param hash $aParameters The action's parameters name => value
|
||||
*/
|
||||
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
|
||||
{
|
||||
if (!is_subclass_of($sCurrentStepClass, WizardStep::class)) {
|
||||
throw new Exception('Unknown step '.$sCurrentStepClass);
|
||||
}
|
||||
return new $sCurrentStepClass($this, $sCurrentState);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Example of a simple Setup Wizard with some parameters to store
|
||||
* the installation mode (install | upgrade) and a simple asynchronous
|
||||
* (AJAX) action.
|
||||
*
|
||||
* The setup wizard is executed by the following code:
|
||||
*
|
||||
* $oWizard = new WizardController('Step1');
|
||||
* $oWizard->Run();
|
||||
*
|
||||
class Step1 extends WizardStep
|
||||
{
|
||||
public function GetTitle()
|
||||
{
|
||||
return 'Welcome';
|
||||
}
|
||||
|
||||
public function GetPossibleSteps()
|
||||
{
|
||||
return array('Step2', 'Step2bis');
|
||||
}
|
||||
|
||||
public function ProcessParams($bMoveForward = true)
|
||||
{
|
||||
$sNextStep = '';
|
||||
$sInstallMode = utils::ReadParam('install_mode');
|
||||
if ($sInstallMode == 'install')
|
||||
{
|
||||
$this->oWizard->SetParameter('install_mode', 'install');
|
||||
$sNextStep = 'Step2';
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->oWizard->SetParameter('install_mode', 'upgrade');
|
||||
$sNextStep = 'Step2bis';
|
||||
|
||||
}
|
||||
return array('class' => $sNextStep, 'state' => '');
|
||||
}
|
||||
|
||||
public function Display(WebPage $oPage)
|
||||
{
|
||||
$oPage->p('This is Step 1!');
|
||||
$sInstallMode = $this->oWizard->GetParameter('install_mode', 'install');
|
||||
$sChecked = ($sInstallMode == 'install') ? ' checked ' : '';
|
||||
$oPage->p('<input type="radio" name="install_mode" value="install"'.$sChecked.'/> Install');
|
||||
$sChecked = ($sInstallMode == 'upgrade') ? ' checked ' : '';
|
||||
$oPage->p('<input type="radio" name="install_mode" value="upgrade"'.$sChecked.'/> Upgrade');
|
||||
}
|
||||
}
|
||||
|
||||
class Step2 extends WizardStep
|
||||
{
|
||||
public function GetTitle()
|
||||
{
|
||||
return 'Installation Parameters';
|
||||
}
|
||||
|
||||
public function GetPossibleSteps()
|
||||
{
|
||||
return array('Step3');
|
||||
}
|
||||
|
||||
public function ProcessParams($bMoveForward = true)
|
||||
{
|
||||
return array('class' => 'Step3', 'state' => '');
|
||||
}
|
||||
|
||||
public function Display(WebPage $oPage)
|
||||
{
|
||||
$oPage->p('This is Step 2! (Installation)');
|
||||
}
|
||||
}
|
||||
|
||||
class Step2bis extends WizardStep
|
||||
{
|
||||
public function GetTitle()
|
||||
{
|
||||
return 'Upgrade Parameters';
|
||||
}
|
||||
|
||||
public function GetPossibleSteps()
|
||||
{
|
||||
return array('Step2ter');
|
||||
}
|
||||
|
||||
public function ProcessParams($bMoveForward = true)
|
||||
{
|
||||
$sUpgradeInfo = utils::ReadParam('upgrade_info');
|
||||
$this->oWizard->SetParameter('upgrade_info', $sUpgradeInfo);
|
||||
$sAdditionalUpgradeInfo = utils::ReadParam('additional_upgrade_info');
|
||||
$this->oWizard->SetParameter('additional_upgrade_info', $sAdditionalUpgradeInfo);
|
||||
return array('class' => 'Step2ter', 'state' => '');
|
||||
}
|
||||
|
||||
public function Display(WebPage $oPage)
|
||||
{
|
||||
$oPage->p('This is Step 2bis! (Upgrade)');
|
||||
$sUpgradeInfo = $this->oWizard->GetParameter('upgrade_info', '');
|
||||
$oPage->p('Type your name here: <input type="text" id="upgrade_info" name="upgrade_info" value="'.$sUpgradeInfo.'" size="20"/><span id="v_upgrade_info"></span>');
|
||||
$sAdditionalUpgradeInfo = $this->oWizard->GetParameter('additional_upgrade_info', '');
|
||||
$oPage->p('The installer replies: <input type="text" name="additional_upgrade_info" value="'.$sAdditionalUpgradeInfo.'" size="20"/>');
|
||||
|
||||
$oPage->add_ready_script("$('#upgrade_info').change(function() {
|
||||
$('#v_upgrade_info').html('<img src=\"../images/indicator.gif\"/>');
|
||||
WizardAsyncAction('', { upgrade_info: $('#upgrade_info').val() }); });");
|
||||
}
|
||||
|
||||
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
|
||||
{
|
||||
usleep(300000); // 300 ms
|
||||
$sName = $aParameters['upgrade_info'];
|
||||
$sReply = addslashes("Hello ".$sName);
|
||||
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
$("#v_upgrade_info").html('');
|
||||
$("input[name=additional_upgrade_info]").val("$sReply");
|
||||
EOF
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Step2ter extends WizardStep
|
||||
{
|
||||
public function GetTitle()
|
||||
{
|
||||
return 'Additional Upgrade Info';
|
||||
}
|
||||
|
||||
public function GetPossibleSteps()
|
||||
{
|
||||
return array('Step3');
|
||||
}
|
||||
|
||||
public function ProcessParams($bMoveForward = true)
|
||||
{
|
||||
return array('class' => 'Step3', 'state' => '');
|
||||
}
|
||||
|
||||
public function Display(WebPage $oPage)
|
||||
{
|
||||
$oPage->p('This is Step 2ter! (Upgrade)');
|
||||
}
|
||||
}
|
||||
|
||||
class Step3 extends WizardStep
|
||||
{
|
||||
public function GetTitle()
|
||||
{
|
||||
return 'Installation Complete';
|
||||
}
|
||||
|
||||
public function GetPossibleSteps()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function ProcessParams($bMoveForward = true)
|
||||
{
|
||||
return array('class' => '', 'state' => '');
|
||||
}
|
||||
|
||||
public function Display(WebPage $oPage)
|
||||
{
|
||||
$oPage->p('This is the FINAL Step');
|
||||
}
|
||||
|
||||
public function CanMoveForward()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
End of the example */
|
||||
|
||||
2539
setup/wizardsteps.class.inc.php
Normal file
2539
setup/wizardsteps.class.inc.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,111 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2013-2026 Combodo SAS
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
abstract class AbstractWizStepInstall extends WizardStep
|
||||
{
|
||||
/**
|
||||
* Prepare the parameters to execute the installation asynchronously
|
||||
* @return array A big hash array that can be converted to XML or JSON with all the needed parameters
|
||||
*/
|
||||
protected function BuildConfig()
|
||||
{
|
||||
$sMode = $this->oWizard->GetParameter('install_mode', 'install');
|
||||
$aSelectedModules = json_decode($this->oWizard->GetParameter('selected_modules'), true);
|
||||
$aSelectedExtensions = json_decode($this->oWizard->GetParameter('selected_extensions'), true);
|
||||
$sBackupDestination = '';
|
||||
$sPreviousConfigurationFile = '';
|
||||
$sDBName = $this->oWizard->GetParameter('db_name');
|
||||
if ($sMode == 'upgrade') {
|
||||
$sPreviousVersionDir = $this->oWizard->GetParameter('previous_version_dir', '');
|
||||
if (!empty($sPreviousVersionDir)) {
|
||||
$aPreviousInstance = SetupUtils::GetPreviousInstance($sPreviousVersionDir);
|
||||
if ($aPreviousInstance['found']) {
|
||||
$sPreviousConfigurationFile = $aPreviousInstance['configuration_file'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->oWizard->GetParameter('db_backup', false)) {
|
||||
$sBackupDestination = $this->oWizard->GetParameter('db_backup_path', '');
|
||||
}
|
||||
} else {
|
||||
|
||||
$sDBNewName = $this->oWizard->GetParameter('db_new_name', '');
|
||||
if ($sDBNewName != '') {
|
||||
$sDBName = $sDBNewName; // Database will be created
|
||||
}
|
||||
}
|
||||
|
||||
$sSourceDir = $this->oWizard->GetParameter('source_dir');
|
||||
$aCopies = [];
|
||||
if (($sMode == 'upgrade') && ($this->oWizard->GetParameter('upgrade_type') == 'keep-previous')) {
|
||||
$sPreviousVersionDir = $this->oWizard->GetParameter('previous_version_dir');
|
||||
$aCopies[] = ['source' => $sSourceDir, 'destination' => 'modules']; // Source is an absolute path, destination is relative to APPROOT
|
||||
$aCopies[] = ['source' => $sPreviousVersionDir.'/portal', 'destination' => 'portal']; // Source is an absolute path, destination is relative to APPROOT
|
||||
$sSourceDir = APPROOT.'modules';
|
||||
}
|
||||
|
||||
$aInstallParams = [
|
||||
'mode' => $sMode,
|
||||
'preinstall' => [
|
||||
'copies' => $aCopies,
|
||||
// 'backup' => see below
|
||||
],
|
||||
'source_dir' => str_replace(APPROOT, '', $sSourceDir),
|
||||
'datamodel_version' => $this->oWizard->GetParameter('datamodel_version'), //TODO: let the installer compute this automatically...
|
||||
'previous_configuration_file' => $sPreviousConfigurationFile,
|
||||
'extensions_dir' => 'extensions',
|
||||
'target_env' => 'production',
|
||||
'workspace_dir' => '',
|
||||
'database' => [
|
||||
'server' => $this->oWizard->GetParameter('db_server'),
|
||||
'user' => $this->oWizard->GetParameter('db_user'),
|
||||
'pwd' => $this->oWizard->GetParameter('db_pwd'),
|
||||
'name' => $sDBName,
|
||||
'db_tls_enabled' => $this->oWizard->GetParameter('db_tls_enabled'),
|
||||
'db_tls_ca' => $this->oWizard->GetParameter('db_tls_ca'),
|
||||
'prefix' => $this->oWizard->GetParameter('db_prefix'),
|
||||
],
|
||||
'url' => $this->oWizard->GetParameter('application_url'),
|
||||
'graphviz_path' => $this->oWizard->GetParameter('graphviz_path'),
|
||||
'admin_account' => [
|
||||
'user' => $this->oWizard->GetParameter('admin_user'),
|
||||
'pwd' => $this->oWizard->GetParameter('admin_pwd'),
|
||||
'language' => $this->oWizard->GetParameter('admin_language'),
|
||||
],
|
||||
'language' => $this->oWizard->GetParameter('default_language'),
|
||||
'selected_modules' => $aSelectedModules,
|
||||
'selected_extensions' => $aSelectedExtensions,
|
||||
'sample_data' => $this->oWizard->GetParameter('sample_data', '') === 'yes',
|
||||
'old_addon' => $this->oWizard->GetParameter('old_addon', false), // whether or not to use the "old" userrights profile addon
|
||||
'options' => json_decode($this->oWizard->GetParameter('misc_options', '[]'), true),
|
||||
'mysql_bindir' => $this->oWizard->GetParameter('mysql_bindir'),
|
||||
];
|
||||
|
||||
if ($sBackupDestination != '') {
|
||||
$aInstallParams['preinstall']['backup'] = [
|
||||
'destination' => $sBackupDestination,
|
||||
'configuration_file' => $sPreviousConfigurationFile,
|
||||
];
|
||||
}
|
||||
|
||||
return $aInstallParams;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2013-2026 Combodo SAS
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
/**
|
||||
* @since 3.0.0 N°4092
|
||||
*/
|
||||
abstract class AbstractWizStepMiscParams extends WizardStep
|
||||
{
|
||||
/**
|
||||
* @since 3.0.0 N°4092
|
||||
*/
|
||||
final protected function AddUseSymlinksFlagOption(WebPage $oPage): void
|
||||
{
|
||||
if (MFCompiler::CanUseSymbolicLinksFlagBeUsed()) {
|
||||
$sChecked = (MFCompiler::IsUseSymbolicLinksFlagPresent()) ? ' checked' : '';
|
||||
|
||||
$oPage->add('<fieldset>');
|
||||
$oPage->add('<legend>Dev parameters</legend>');
|
||||
$oPage->p('<input id="use-symbolic-links" type="checkbox"'.$sChecked.'><label for="use-symbolic-links"> Create symbolic links instead of creating a copy in env-production (useful for debugging extensions)');
|
||||
$oPage->add('</fieldset>');
|
||||
$oPage->add_ready_script(
|
||||
<<<'JS'
|
||||
$("#use-symbolic-links").on("click", function() {
|
||||
var $this = $(this),
|
||||
bUseSymbolicLinks = $this.prop("checked");
|
||||
var sAuthent = $('#authent_token').val();
|
||||
var oAjaxParams = { operation: 'toggle_use_symbolic_links', bUseSymbolicLinks: bUseSymbolicLinks, authent: sAuthent};
|
||||
$.post(GetAbsoluteUrlAppRoot()+'setup/ajax.dataloader.php', oAjaxParams);
|
||||
});
|
||||
JS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final protected function AddForceUninstallFlagOption(WebPage $oPage): void
|
||||
{
|
||||
$sChecked = $this->oWizard->GetParameter('force-uninstall', false) ? ' checked ' : '';
|
||||
$oPage->add('<fieldset>');
|
||||
$oPage->add('<legend>Advanced parameters</legend>');
|
||||
$oPage->p('<input id="force-uninstall" type="checkbox"'.$sChecked.' name="force-uninstall"><label for="force-uninstall"> Disable uninstallation checks for extensions');
|
||||
$oPage->add('</fieldset>');
|
||||
|
||||
$oPage->add_ready_script(
|
||||
<<<'JS'
|
||||
$("#force-uninstall").on("click", function() {
|
||||
let $this = $(this);
|
||||
let bForceUninstall = $this.prop("checked");
|
||||
if( bForceUninstall && !confirm('Beware, uninstalling extensions flagged as non uninstallable may result in data corruption and application crashes. Are you sure you want to continue ?')){
|
||||
$this.prop("checked",false);
|
||||
}
|
||||
});
|
||||
JS
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2013-2026 Combodo SAS
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
/**
|
||||
* Administrator Account definition screen
|
||||
*/
|
||||
class WizStepAdminAccount extends WizardStep
|
||||
{
|
||||
public function GetTitle()
|
||||
{
|
||||
return 'Administrator Account';
|
||||
}
|
||||
|
||||
public function GetPossibleSteps()
|
||||
{
|
||||
return [WizStepInstallMiscParams::class];
|
||||
}
|
||||
|
||||
public function UpdateWizardStateAndGetNextStep($bMoveForward = true)
|
||||
{
|
||||
$this->oWizard->SaveParameter('admin_user', '');
|
||||
$this->oWizard->SaveParameter('admin_pwd', '');
|
||||
$this->oWizard->SaveParameter('confirm_pwd', '');
|
||||
$this->oWizard->SaveParameter('admin_language', 'EN US');
|
||||
|
||||
return ['class' => WizStepInstallMiscParams::class, 'state' => ''];
|
||||
}
|
||||
|
||||
public function Display(WebPage $oPage)
|
||||
{
|
||||
$sAdminUser = $this->oWizard->GetParameter('admin_user', 'admin');
|
||||
$sAdminPwd = $this->oWizard->GetParameter('admin_pwd', '');
|
||||
$sConfirmPwd = $this->oWizard->GetParameter('confirm_pwd', '');
|
||||
$sAdminLanguage = $this->oWizard->GetParameter('admin_language', 'EN US');
|
||||
$oPage->add('<h2>Definition of the Administrator Account</h2>');
|
||||
$oPage->add('<fieldset>');
|
||||
$oPage->add('<legend>Administrator Account</legend>');
|
||||
$oPage->add('<table>');
|
||||
$oPage->add('<tr><td>Login: </td><td><input id="admin_user" class="ibo-input" name="admin_user" type="text" size="25" maxlength="64" value="'.utils::EscapeHtml($sAdminUser).'"><span id="v_admin_user"/></td></tr>');
|
||||
$oPage->add('<tr><td>Password: </td><td><input id="admin_pwd" class="ibo-input" autocomplete="off" name="admin_pwd" type="password" size="25" maxlength="64" value="'.utils::EscapeHtml($sAdminPwd).'"><span id="v_admin_pwd"/></td></tr>');
|
||||
$oPage->add('<tr><td>Confirm password: </td><td><input id="confirm_pwd" class="ibo-input" autocomplete="off" name="confirm_pwd" type="password" size="25" maxlength="64" value="'.utils::EscapeHtml($sConfirmPwd).'"></td></tr>');
|
||||
$sSourceDir = APPROOT.'dictionaries/';
|
||||
$aLanguages = SetupUtils::GetAvailableLanguages($sSourceDir);
|
||||
$oPage->add('<tr><td>Language: </td><td>');
|
||||
$oPage->add(SetupUtils::GetLanguageSelect($sSourceDir, 'admin_language', $sAdminLanguage));
|
||||
$oPage->add('</td></tr>');
|
||||
$oPage->add('</table>');
|
||||
$oPage->add('</fieldset>');
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
$('#admin_user').on('change keyup', function() { WizardUpdateButtons(); } );
|
||||
$('#admin_pwd').on('change keyup', function() { WizardUpdateButtons(); } );
|
||||
$('#confirm_pwd').on('change keyup', function() { WizardUpdateButtons(); } );
|
||||
EOF
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the "Next" button should be enabled interactively
|
||||
* @return string A piece of javascript code returning either true or false
|
||||
*/
|
||||
public function JSCanMoveForward()
|
||||
{
|
||||
return
|
||||
<<<EOF
|
||||
bRet = ($('#admin_user').val() != '');
|
||||
if (!bRet)
|
||||
{
|
||||
$("#v_admin_user").html('<i class="fas fa-exclamation-triangle setup-invalid-field--icon" title="This field cannot be empty"></i>');
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#v_admin_user").html('');
|
||||
}
|
||||
|
||||
bPasswordsMatch = ($('#admin_pwd').val() == $('#confirm_pwd').val());
|
||||
if (!bPasswordsMatch)
|
||||
{
|
||||
$('#v_admin_pwd').html('<i class="fas fa-exclamation-triangle setup-invalid-field--icon" title="Retyped password does not match"></i>');
|
||||
}
|
||||
else
|
||||
{
|
||||
$('#v_admin_pwd').html('');
|
||||
}
|
||||
bRet = bPasswordsMatch && bRet;
|
||||
|
||||
return bRet;
|
||||
EOF
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2013-2026 Combodo SAS
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
/**
|
||||
* Database Connection parameters screen
|
||||
*/
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
class WizStepDBParams extends WizardStep
|
||||
{
|
||||
public function GetTitle()
|
||||
{
|
||||
return 'Database Configuration';
|
||||
}
|
||||
|
||||
public function GetPossibleSteps()
|
||||
{
|
||||
return [WizStepAdminAccount::class];
|
||||
}
|
||||
|
||||
public function UpdateWizardStateAndGetNextStep($bMoveForward = true)
|
||||
{
|
||||
$this->oWizard->SaveParameter('db_server', '');
|
||||
$this->oWizard->SaveParameter('db_user', '');
|
||||
$this->oWizard->SaveParameter('db_pwd', '');
|
||||
$this->oWizard->SaveParameter('db_name', '');
|
||||
$this->oWizard->SaveParameter('db_prefix', '');
|
||||
$this->oWizard->SaveParameter('new_db_name', '');
|
||||
$this->oWizard->SaveParameter('create_db', '');
|
||||
$this->oWizard->SaveParameter('db_new_name', '');
|
||||
$this->oWizard->SaveParameter('db_tls_enabled', false);
|
||||
$this->oWizard->SaveParameter('db_tls_ca', '');
|
||||
|
||||
return ['class' => WizStepAdminAccount::class, 'state' => ''];
|
||||
}
|
||||
|
||||
public function Display(WebPage $oPage)
|
||||
{
|
||||
$oPage->add('<h2>Configuration of the database connection:</h2>');
|
||||
$sDBServer = $this->oWizard->GetParameter('db_server', '');
|
||||
$sDBUser = $this->oWizard->GetParameter('db_user', '');
|
||||
$sDBPwd = $this->oWizard->GetParameter('db_pwd', '');
|
||||
$sDBName = $this->oWizard->GetParameter('db_name', '');
|
||||
$sDBPrefix = $this->oWizard->GetParameter('db_prefix', '');
|
||||
$sTlsEnabled = $this->oWizard->GetParameter('db_tls_enabled', '');
|
||||
$sTlsCA = $this->oWizard->GetParameter('db_tls_ca', '');
|
||||
$sNewDBName = $this->oWizard->GetParameter('db_new_name', false);
|
||||
|
||||
$oPage->add('<table>');
|
||||
SetupUtils::DisplayDBParameters(
|
||||
$oPage,
|
||||
true,
|
||||
$sDBServer,
|
||||
$sDBUser,
|
||||
$sDBPwd,
|
||||
$sDBName,
|
||||
$sDBPrefix,
|
||||
$sTlsEnabled,
|
||||
$sTlsCA,
|
||||
$sNewDBName
|
||||
);
|
||||
$sAuthentToken = $this->oWizard->GetParameter('authent', '');
|
||||
$oPage->add('<input type="hidden" id="authent_token" value="'.$sAuthentToken.'"/>');
|
||||
$oPage->add('</table>');
|
||||
$sCreateDB = $this->oWizard->GetParameter('create_db', 'yes');
|
||||
if ($sCreateDB == 'no') {
|
||||
$oPage->add_ready_script('$("#existing_db").prop("checked", true);');
|
||||
} else {
|
||||
$oPage->add_ready_script('$("#create_db").prop("checked", true);');
|
||||
}
|
||||
}
|
||||
|
||||
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
|
||||
{
|
||||
switch ($sCode) {
|
||||
case 'check_db':
|
||||
SetupUtils::AsyncCheckDB($oPage, $aParameters);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the "Next" button should be enabled interactively
|
||||
* @return string A piece of javascript code returning either true or false
|
||||
*/
|
||||
public function JSCanMoveForward()
|
||||
{
|
||||
return
|
||||
<<<EOF
|
||||
if ($("#wiz_form").data("db_connection") === "error") return false;
|
||||
|
||||
var bRet = true;
|
||||
bRet = ValidateField("db_name", true) && bRet;
|
||||
bRet = ValidateField("db_new_name", true) && bRet;
|
||||
bRet = ValidateField("db_prefix", true) && bRet;
|
||||
|
||||
return bRet;
|
||||
EOF
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2013-2026 Combodo SAS
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
require_once(APPROOT.'setup/sequencers/DataAuditSequencer.php');
|
||||
|
||||
/**
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class WizStepDataAudit extends WizStepInstall
|
||||
{
|
||||
public const SequencerClass = DataAuditSequencer::class;
|
||||
|
||||
public function GetTitle()
|
||||
{
|
||||
return 'Checking compatibility';
|
||||
|
||||
}
|
||||
|
||||
public function GetPossibleSteps()
|
||||
{
|
||||
return [WizStepSummary::class];
|
||||
}
|
||||
|
||||
public function GetNextButtonLabel()
|
||||
{
|
||||
return 'Next';
|
||||
}
|
||||
|
||||
public function CanMoveForward()
|
||||
{
|
||||
if ($this->CheckDependencies()) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function UpdateWizardStateAndGetNextStep($bMoveForward = true)
|
||||
{
|
||||
return ['class' => WizStepSummary::class, 'state' => ''];
|
||||
}
|
||||
|
||||
public function CanComeBack()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function Display(WebPage $oPage)
|
||||
{
|
||||
|
||||
$aInstallParams = $this->BuildConfig();
|
||||
|
||||
$this->AddProgressBar($oPage);
|
||||
|
||||
$sJSONData = json_encode($aInstallParams);
|
||||
$oPage->add('<input type="hidden" id="installer_parameters" value="'.utils::EscapeHtml($sJSONData).'"/>');
|
||||
|
||||
$sAuthentToken = $this->oWizard->GetParameter('authent', '');
|
||||
$oPage->add('<input type="hidden" id="authent_token" value="'.$sAuthentToken.'"/>');
|
||||
$sApplicationUrl = $this->oWizard->GetParameter('application_url').'pages/exec.php?exec_module=combodo-data-feature-removal&exec_page=index.php';
|
||||
$oPage->add('<input type="hidden" id="application_url" value="'.$sApplicationUrl.'"/>');
|
||||
|
||||
if (!$this->CheckDependencies()) {
|
||||
$oPage->error($this->sDependencyIssue);
|
||||
}
|
||||
|
||||
$oPage->add_ready_script(
|
||||
<<<JS
|
||||
$("#wiz_form").data("installation_status", "not started");
|
||||
ExecuteStep("");
|
||||
JS
|
||||
);
|
||||
}
|
||||
|
||||
protected function AddProgressErrorScript($oPage, $aRes)
|
||||
{
|
||||
if (isset($aRes['error_code']) && $aRes['error_code'] === DataAuditSequencer::DATA_AUDIT_FAILED) {
|
||||
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
$('.ibo-setup--wizard--buttons-container tr td:nth-child(2)').before('<td style="text-align:center;"><button class="ibo-button ibo-is-alternative ibo-is-neutral" type="submit" name="operation" value="next"><span class="ibo-button--label">Ignore and continue</span></button></td>');
|
||||
|
||||
$('.ibo-setup--wizard--buttons-container tr td:nth-child(2)').after('<td style="text-align:center;"><a href="'+$('#application_url').val()+'"><button class="default ibo-button ibo-is-regular ibo-is-primary" type="button"><span class="ibo-button--label">Go to backoffice</span></button></a></td>');
|
||||
|
||||
$("#wiz_form").data("installation_status", "cleanup_needed");
|
||||
$('#btn_next').hide();
|
||||
EOF
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function JSCanMoveForward()
|
||||
{
|
||||
return 'return ["completed", "cleanup_needed"].indexOf($("#wiz_form").data("installation_status")) !== -1;';
|
||||
}
|
||||
|
||||
public function JSCanMoveBackward()
|
||||
{
|
||||
return 'return ["not started", "error", "cleanup_needed"].indexOf($("#wiz_form").data("installation_status")) !== -1;';
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2013-2026 Combodo SAS
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
/**
|
||||
* Upgrade information
|
||||
*/
|
||||
class WizStepDetectedInfo extends WizardStep
|
||||
{
|
||||
protected $bCanMoveForward;
|
||||
|
||||
public function GetTitle()
|
||||
{
|
||||
return 'Upgrade Information';
|
||||
}
|
||||
|
||||
public function GetPossibleSteps()
|
||||
{
|
||||
return [WizStepUpgradeMiscParams::class, WizStepLicense2::class];
|
||||
}
|
||||
|
||||
public function UpdateWizardStateAndGetNextStep($bMoveForward = true)
|
||||
{
|
||||
$sUpgradeType = utils::ReadParam('upgrade_type');
|
||||
|
||||
$this->oWizard->SetParameter('mode', 'upgrade');
|
||||
$this->oWizard->SetParameter('upgrade_type', $sUpgradeType);
|
||||
$bDisplayLicense = $this->oWizard->GetParameter('display_license');
|
||||
|
||||
switch ($sUpgradeType) {
|
||||
case 'keep-previous':
|
||||
$sSourceDir = utils::ReadParam('relative_source_dir', '', false, 'raw_data');
|
||||
$this->oWizard->SetParameter('source_dir', $this->oWizard->GetParameter('previous_version_dir').'/'.$sSourceDir);
|
||||
$this->oWizard->SetParameter('datamodel_version', utils::ReadParam('datamodel_previous_version', '', false, 'raw_data'));
|
||||
break;
|
||||
|
||||
case 'use-compatible':
|
||||
$sDataModelPath = utils::ReadParam('datamodel_path', '', false, 'raw_data');
|
||||
$this->oWizard->SetParameter('source_dir', $sDataModelPath);
|
||||
$this->oWizard->SaveParameter('datamodel_version', '');
|
||||
break;
|
||||
|
||||
default:
|
||||
// Do nothing, maybe the user pressed the Back button
|
||||
}
|
||||
if ($bDisplayLicense) {
|
||||
$aRet = ['class' => WizStepLicense2::class, 'state' => ''];
|
||||
} else {
|
||||
$aRet = ['class' => WizStepUpgradeMiscParams::class, 'state' => ''];
|
||||
}
|
||||
return $aRet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oPage
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function Display(WebPage $oPage)
|
||||
{
|
||||
$oPage->add_style(
|
||||
<<<EOF
|
||||
#changes_summary {
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
#changes_summary div {
|
||||
width:100;
|
||||
margin-top:0;
|
||||
padding-top: 0.5em;
|
||||
padding-left: 0;
|
||||
}
|
||||
#changes_summary div ul {
|
||||
margin-left:0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
#changes_summary div.closed ul {
|
||||
display:none;
|
||||
}
|
||||
#changes_summary div li {
|
||||
list-style: none;
|
||||
width: 100;
|
||||
margin-left:0;
|
||||
padding-left: 0em;
|
||||
}
|
||||
.title {
|
||||
padding-left: 20px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: url(../images/minus.gif) 2px 2px no-repeat;
|
||||
}
|
||||
#changes_summary div.closed .title {
|
||||
background: url(../images/plus.gif) 2px 2px no-repeat;
|
||||
}
|
||||
EOF
|
||||
);
|
||||
$this->bCanMoveForward = true;
|
||||
$bDisplayLicense = true;
|
||||
$sPreviousVersionDir = $this->oWizard->GetParameter('previous_version_dir', '');
|
||||
$aInstalledInfo = SetupUtils::GetApplicationVersion($this->oWizard);
|
||||
|
||||
if ($aInstalledInfo === false) {
|
||||
throw(new Exception('No previous version of '.ITOP_APPLICATION.' found in the supplied database. The upgrade cannot continue.'));
|
||||
} elseif (strcasecmp($aInstalledInfo['product_name'], ITOP_APPLICATION) != 0) {
|
||||
$oPage->p("<b>Warning: The installed products seem different. Are you sure that you want to upgrade {$aInstalledInfo['product_name']} with ".ITOP_APPLICATION."?</b>");
|
||||
}
|
||||
|
||||
$sInstalledVersion = $aInstalledInfo['product_version'];
|
||||
$sInstalledDataModelVersion = $aInstalledInfo['datamodel_version'];
|
||||
|
||||
$oPage->add("<h2>Information about the upgrade from version $sInstalledVersion to ".ITOP_VERSION_FULL."</h2>");
|
||||
|
||||
if ($sInstalledVersion == ITOP_VERSION_FULL) {
|
||||
// Reinstalling the same version let's skip the license agreement...
|
||||
$bDisplayLicense = false;
|
||||
}
|
||||
$this->oWizard->SetParameter('display_license', $bDisplayLicense); // Remember for later
|
||||
|
||||
$sCompatibleDMDir = SetupUtils::GetLatestDataModelDir();
|
||||
if ($sCompatibleDMDir === false) {
|
||||
// No compatible version exists... cannot upgrade. Either it is too old, or too new (downgrade !)
|
||||
$this->bCanMoveForward = false;
|
||||
$oPage->p("No datamodel directory found.");
|
||||
} else {
|
||||
$sUpgradeDMVersion = SetupUtils::GetDataModelVersion($sCompatibleDMDir);
|
||||
$sPreviousSourceDir = isset($aInstalledInfo['source_dir']) ? $aInstalledInfo['source_dir'] : 'modules';
|
||||
$aChanges = false;
|
||||
if (is_dir($sPreviousVersionDir)) {
|
||||
// Check if the previous version is a "genuine" one or not...
|
||||
$aChanges = SetupUtils::CheckVersion($sInstalledDataModelVersion, $sPreviousVersionDir.'/'.$sPreviousSourceDir);
|
||||
}
|
||||
if (($aChanges !== false) && ((count($aChanges['added']) > 0) || (count($aChanges['removed']) > 0) || (count($aChanges['modified']) > 0))) {
|
||||
// Some changes were detected, prompt the user to keep or discard them
|
||||
$oPage->p("<img src=\"../images/error.png\"/> Some modifications were detected between the ".ITOP_APPLICATION." version in '$sPreviousVersionDir' and a genuine $sInstalledVersion version.");
|
||||
$oPage->p("What do you want to do?");
|
||||
|
||||
$aWritableDirs = ['modules', 'portal'];
|
||||
$aErrors = SetupUtils::CheckWritableDirs($aWritableDirs);
|
||||
$sChecked = ($this->oWizard->GetParameter('upgrade_type') == 'keep-previous') ? ' checked ' : '';
|
||||
$sDisabled = (count($aErrors) > 0) ? ' disabled ' : '';
|
||||
|
||||
$oPage->p('<input id="radio_upgrade_keep" type="radio" name="upgrade_type" value="keep-previous" '.$sChecked.$sDisabled.'/><label for="radio_upgrade_keep"> Preserve the modifications of the installed version (the dashboards inside '.ITOP_APPLICATION.' may not be editable).</label>');
|
||||
$oPage->add('<input type="hidden" name="datamodel_previous_version" value="'.utils::EscapeHtml($sInstalledDataModelVersion).'">');
|
||||
|
||||
$oPage->add('<input type="hidden" name="relative_source_dir" value="'.utils::EscapeHtml($sPreviousSourceDir).'">');
|
||||
|
||||
if (count($aErrors) > 0) {
|
||||
$oPage->p("Cannot copy the installed version due to the following access rights issue(s):");
|
||||
foreach ($aErrors as $sDir => $oCheckResult) {
|
||||
$oPage->p('<img src="../images/error.png"/> '.$oCheckResult->sLabel);
|
||||
}
|
||||
}
|
||||
|
||||
$sChecked = ($this->oWizard->GetParameter('upgrade_type') == 'use-compatible') ? ' checked ' : '';
|
||||
|
||||
$oPage->p('<input id="radio_upgrade_convert" type="radio" name="upgrade_type" value="use-compatible" '.$sChecked.'/><label for="radio_upgrade_convert"> Discard the modifications, use a standard '.$sUpgradeDMVersion.' data model.</label>');
|
||||
|
||||
$oPage->add('<input type="hidden" name="datamodel_path" value="'.utils::EscapeHtml($sCompatibleDMDir).'">');
|
||||
$oPage->add('<input type="hidden" name="datamodel_version" value="'.utils::EscapeHtml($sUpgradeDMVersion).'">');
|
||||
|
||||
$oPage->add('<div id="changes_summary"><div class="closed"><span class="title">Details of the modifications</span><div>');
|
||||
if (count($aChanges['added']) > 0) {
|
||||
$oPage->add('<ul>New files added:');
|
||||
foreach ($aChanges['added'] as $sFilePath => $void) {
|
||||
$oPage->add('<li>'.$sFilePath.'</li>');
|
||||
}
|
||||
$oPage->add('</ul>');
|
||||
}
|
||||
if (count($aChanges['removed']) > 0) {
|
||||
$oPage->add('<ul>Deleted files:');
|
||||
foreach ($aChanges['removed'] as $sFilePath => $void) {
|
||||
$oPage->add('<li>'.$sFilePath.'</li>');
|
||||
}
|
||||
$oPage->add('</ul>');
|
||||
}
|
||||
if (count($aChanges['modified']) > 0) {
|
||||
$oPage->add('<ul>Modified files:');
|
||||
foreach ($aChanges['modified'] as $sFilePath => $void) {
|
||||
$oPage->add('<li>'.$sFilePath.'</li>');
|
||||
}
|
||||
$oPage->add('</ul>');
|
||||
}
|
||||
$oPage->add('</div></div></div>');
|
||||
} else {
|
||||
// No changes detected... or no way to tell because of the lack of a manifest or previous source dir
|
||||
// Use the "compatible" datamodel as-is.
|
||||
$sCompatibleDMDirToDisplay = utils::HtmlEntities($sCompatibleDMDir);
|
||||
$sUpgradeDMVersionToDisplay = utils::HtmlEntities($sUpgradeDMVersion);
|
||||
$oPage->add(
|
||||
<<<HTML
|
||||
<div class="message message-valid">The datamodel will be upgraded from version $sInstalledDataModelVersion to version $sUpgradeDMVersion.</div>
|
||||
<input type="hidden" name="upgrade_type" value="use-compatible">
|
||||
<input type="hidden" name="datamodel_path" value="$sCompatibleDMDirToDisplay">
|
||||
<input type="hidden" name="datamodel_version" value="$sUpgradeDMVersionToDisplay">
|
||||
HTML
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
$("#changes_summary .title").on('click', function() { $(this).parent().toggleClass('closed'); } );
|
||||
$('input[name=upgrade_type]').on('click change', function() { WizardUpdateButtons(); });
|
||||
EOF
|
||||
);
|
||||
|
||||
$oMutex = new iTopMutex(
|
||||
'cron'.$this->oWizard->GetParameter('db_name', '').$this->oWizard->GetParameter('db_prefix', ''),
|
||||
$this->oWizard->GetParameter('db_server', ''),
|
||||
$this->oWizard->GetParameter('db_user', ''),
|
||||
$this->oWizard->GetParameter('db_pwd', ''),
|
||||
$this->oWizard->GetParameter('db_tls_enabled', ''),
|
||||
$this->oWizard->GetParameter('db_tls_ca', '')
|
||||
);
|
||||
if ($oMutex->IsLocked()) {
|
||||
$oPage->add('<div class="message">'.ITOP_APPLICATION.' cron process is being executed on the target database. '.ITOP_APPLICATION.' cron process will be stopped during the setup execution.</div>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function CanMoveForward()
|
||||
{
|
||||
return $this->bCanMoveForward;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the "Next" button should be enabled interactively
|
||||
* @return string A piece of javascript code returning either true or false
|
||||
*/
|
||||
public function JSCanMoveForward()
|
||||
{
|
||||
return
|
||||
<<<EOF
|
||||
if ($("#radio_upgrade_keep").length == 0) return true;
|
||||
|
||||
bRet = ($('input[name=upgrade_type]:checked').length > 0);
|
||||
return bRet;
|
||||
EOF
|
||||
;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user