Compare commits

..

56 Commits

Author SHA1 Message Date
Eric Espie
3c80c93b4f Dashlet extraction 2026-01-16 18:47:28 +01:00
Eric Espie
efc6e6730b Dashlet extraction 2026-01-16 09:57:14 +01:00
Eric Espie
3769bc1024 Dashlet definition for core dashlets 2026-01-16 09:34:07 +01:00
Stephen Abello
e5338f28eb Save actions submit the form, it has yet to validate the form 2026-01-15 15:45:52 +01:00
Stephen Abello
5cfe7fa6eb Allow dashboard to restore state (either saved state or from backend). Restore old state on edition cancel 2026-01-15 11:51:49 +01:00
Stephen Abello
27c16a782c Remove done todo 2026-01-15 11:09:44 +01:00
Stephen Abello
7c21178e1d Handle backend save response in dashboard js 2026-01-15 11:08:20 +01:00
Stephen Abello
3a6e148c11 Prepare steps for custom dashboard handling 2026-01-15 10:56:35 +01:00
Eric Espie
4d9e18890a fix warnings 2026-01-15 10:44:20 +01:00
Eric Espie
50ffaa2b55 Read custom dashboards with grid format 2026-01-15 10:19:33 +01:00
Eric Espie
15c8f5903b save dashboard controller 2026-01-14 16:57:19 +01:00
Stephen Abello
0bc6f5d56a Align posted dashboard data to new data denormalized format 2026-01-14 11:39:56 +01:00
Stephen Abello
d248524cc8 Align posted dashboard data to new data denormalized format 2026-01-14 11:15:15 +01:00
Stephen Abello
410dc152d7 Make cloning dashlet actually use original dashlet data to get a new similar dashlet 2026-01-14 10:01:38 +01:00
Stephen Abello
423413e3a0 Fix form interaction with buttons when screen is smaller 2026-01-14 09:42:37 +01:00
Stephen Abello
8896c576d7 Make form style + button style match mockup 2026-01-14 09:32:11 +01:00
Benjamin DALSASS
975046da17 Add ChoiceImageFormBlock 2026-01-14 08:19:03 +01:00
Benjamin DALSASS
e4a281c3ff Add ChoiceImageFormBlock 2026-01-14 07:32:29 +01:00
Eric Espie
90729f84b6 Polymorphic type 2026-01-13 18:00:34 +01:00
Stephen Abello
c075a5c145 Make dashlet edition / refresh better interact with listener and gridstack library in order to allow multiple edits without breaking 2026-01-13 16:30:00 +01:00
Benjamin DALSASS
cd6d130bcb Move dashboard blocks to itop project and keep demonstrator one 2026-01-13 11:24:56 +01:00
Stephen Abello
7ca2c56dad Send denormalized data to backend in order to persist dashboard 2026-01-13 11:18:18 +01:00
Benjamin DALSASS
dd0ac58643 Prepare property service for form block dashboard 2026-01-13 07:36:47 +01:00
Stephen Abello
df943ec8b5 Change DashletBadge preferred height 2026-01-12 16:28:18 +01:00
Stephen Abello
076a6d0495 Make dashlet position non-random. Use their preferred width/height for now 2026-01-12 16:26:52 +01:00
Stephen Abello
02e59c906b Remove dummy buttons used for development purpose 2026-01-12 16:11:43 +01:00
Stephen Abello
49f91961e7 Make dashlet rendering computing request data 2026-01-12 15:09:14 +01:00
Stephen Abello
441519d71d Rename parameter 2026-01-12 15:08:33 +01:00
Eric Espie
5c75d0ce7c Fix some value types 2026-01-12 13:50:28 +01:00
Eric Espie
2efe80265d Pass form values from dashlet 2026-01-12 12:02:45 +01:00
Eric Espie
b58a64e143 Add Icon selection formBlock 2026-01-12 11:44:44 +01:00
Stephen Abello
ff11aec7fe Make clone dashlet ask for a rendered dashlet to the backend 2026-01-12 11:10:08 +01:00
Benjamin DALSASS
f79bb9d51c Add option display to form (css needs updates)
Add options with_run_button and with_book_button to FormBlockOql
2026-01-12 08:00:17 +01:00
Stephen Abello
3c365fc103 Make add / edit in dashboard interactive, and pass on data 2026-01-09 16:31:36 +01:00
Stephen Abello
7b193dd737 Use npm for gridstack, remove unnecessary folders using our dedicated service 2026-01-09 15:10:10 +01:00
Eric Espie
1538596db8 PHP CS-Fixer 2026-01-09 11:04:38 +01:00
Eric Espie
9d4fc345bc Fix (de)normalized data for Forms 2026-01-09 10:51:32 +01:00
Eric Espie
08c9309572 Fix (de)normalized data for Forms 2026-01-08 17:41:58 +01:00
Eric Espie
d072aa05f1 Prepare (de)normalized data for Forms 2026-01-08 17:28:48 +01:00
Eric Espie
85c1f091e2 Prepare normalized data for Forms 2026-01-08 17:25:52 +01:00
Eric Espie
be6a8abdf4 N°9066 - Split XML serializer into normalizer and encoder 2026-01-08 17:23:06 +01:00
Benjamin DALSASS
60bcf0c85f Change AbstractForm BindingReceivedEvent 2026-01-08 16:02:23 +01:00
Benjamin DALSASS
4a8804b8ac Dashlet form in new dashboard layout (suite) 2026-01-08 15:30:51 +01:00
Benjamin DALSASS
b014b9f638 Dashlet form in new dashboard layout 2026-01-08 14:42:16 +01:00
Eric Espie
1c633c6173 typo 2026-01-08 13:45:11 +01:00
Stephen Abello
cdc95aca7b Dump autoloader 2026-01-08 10:18:30 +01:00
Stephen Abello
b4460999ef Dump autoloader 2026-01-08 10:17:35 +01:00
Stephen Abello
a713e1b56e N°8641 - Dashboard editor front-end first commit for Form SDK integration.
* No dashlet edition
* Dashboard are not persisted
* Unable to load a dashboard from an endpoint (refresh)
* Grid library need proper npm integration
2026-01-08 10:17:30 +01:00
Vincent Dumas
3e879c64a7 N°4032 - On UserRequest, change proposed service subcategories (#786)
* N°4032 - On UserRequest, service subcategory no more limited by request_type
2026-01-08 10:08:52 +01:00
Eric Espie
5c6369b9b8 N°9065 - XML Definition for Dashlet properties 2026-01-07 17:16:50 +01:00
Eric Espie
154fb5c737 N°9066 - Serialization/Unserialization from XML to Forms 2026-01-07 17:09:47 +01:00
Eric Espie
efb1bd765b N°9065 - XML Definition for Dashlet properties 2026-01-07 17:09:20 +01:00
Eric Espie
b39af74d07 Fix CI 2026-01-07 09:04:56 +01:00
Eric Espie
904cd0b518 N°8772 - Move DIService into PSR-11 Service Locator 2025-12-30 14:27:28 +01:00
Benjamin Dalsass
4c1ad0f4f2 N°8772 - Form dependencies manager implementation
- Form SDK implementation
- Basic Forms
- Dynamics Forms
- Basic Blocks + Data Model Block
- Form Compilation
- Turbo integration
2025-12-30 11:42:55 +01:00
v-dumas
3955b4eb22 N°8534 - Prevent ending on Portal 2025-12-22 17:49:47 +01:00
1127 changed files with 144837 additions and 30666 deletions

16
.phpstorm.meta.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace PHPSTORM_META
{
override(\MetaModel::NewObject(0), map([
'' => '@',
]));
override(\MetaModel::GetObject(0), map([
'' => '@',
]));
}

View File

@@ -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');

View File

@@ -5,6 +5,9 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Application\Dashboard\Layout\DashboardLayoutGrid;
use Combodo\iTop\Application\Dashlet\DashletFactory;
use Combodo\iTop\Application\Dashlet\Service\DashletService;
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableSettings;
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu;
@@ -12,9 +15,12 @@ use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Layout\Dashboard\DashboardLayout as DashboardLayoutUIBlock;
use Combodo\iTop\Application\WebPage\iTopWebPage;
use Combodo\iTop\Application\WebPage\WebPage;
use Combodo\iTop\DesignDocument;
use Combodo\iTop\DesignElement;
use Combodo\iTop\PropertyType\PropertyTypeDesign;
use Combodo\iTop\Service\DependencyInjection\ServiceLocator;
require_once(APPROOT.'application/dashboardlayout.class.inc.php');
require_once(APPROOT.'application/dashlet.class.inc.php');
require_once(APPROOT.'core/modelreflection.class.inc.php');
/**
@@ -24,7 +30,7 @@ require_once(APPROOT.'core/modelreflection.class.inc.php');
*/
abstract class Dashboard
{
/** @var string $sTitle*/
/** @var string $sTitle */
protected $sTitle;
/** @var bool $bAutoReload */
protected $bAutoReload;
@@ -34,7 +40,7 @@ abstract class Dashboard
protected $sLayoutClass;
/** @var array $aWidgetsData */
protected $aWidgetsData;
/** @var \DOMNode|null $oDOMNode */
/** @var DesignElement|null $oDOMNode */
protected $oDOMNode;
/** @var string $sId */
protected $sId;
@@ -43,6 +49,11 @@ abstract class Dashboard
/** @var \ModelReflection $oMetaModel */
protected $oMetaModel;
/** @var array Array of dashlets with position */
protected array $aGridDashlets = [];
protected $oDashletFactory;
/**
* Dashboard constructor.
*
@@ -57,6 +68,7 @@ abstract class Dashboard
$this->aCells = [];
$this->oDOMNode = null;
$this->sId = $sId;
$this->oDashletFactory = DashletFactory::GetInstance();
}
/**
@@ -68,7 +80,7 @@ abstract class Dashboard
{
$this->aCells = []; // reset the content of the dashboard
set_error_handler(['Dashboard', 'ErrorHandler']);
$oDoc = new DOMDocument();
$oDoc = new PropertyTypeDesign();
$oDoc->loadXML($sXml);
restore_error_handler();
$this->FromDOMDocument($oDoc);
@@ -77,10 +89,15 @@ abstract class Dashboard
/**
* @param \DOMDocument $oDoc
*/
public function FromDOMDocument(DOMDocument $oDoc)
public function FromDOMDocument(DesignDocument $oDoc)
{
$this->oDOMNode = $oDoc->getElementsByTagName('dashboard')->item(0);
if ($this->oDOMNode->getElementsByTagName('cells')->count() === 0) {
$this->FromDOMDocumentV2($oDoc);
return;
}
if ($oLayoutNode = $this->oDOMNode->getElementsByTagName('layout')->item(0)) {
$this->sLayoutClass = $oLayoutNode->textContent;
} else {
@@ -121,7 +138,7 @@ abstract class Dashboard
$aDashletOrder = [];
/** @var \DOMElement $oDomNode */
foreach ($oDashletList as $oDomNode) {
$oRank = $oDomNode->getElementsByTagName('rank')->item(0);
$oRank = $oDomNode->getElementsByTagName('rank')->item(0);
if ($oRank) {
$iRank = (float)$oRank->textContent;
}
@@ -147,7 +164,39 @@ abstract class Dashboard
}
/**
* @param \DOMElement $oDomNode
* @param \DOMDocument $oDoc
*/
public function FromDOMDocumentV2(DesignDocument $oDoc)
{
$this->oDOMNode = $oDoc->getElementsByTagName('dashboard')->item(0);
$this->sLayoutClass = DashboardLayoutGrid::class;
$this->sTitle = $this->oDOMNode->GetChildText('title', '');
$iRefresh = intval($this->oDOMNode->GetChildText('refresh', '0'));
$this->bAutoReload = $iRefresh > 0;
$this->iAutoReloadSec = $iRefresh;
$oDashletsNode = $this->oDOMNode->GetUniqueElement('pos_dashlets');
$oDashletList = $oDashletsNode->getElementsByTagName('pos_dashlet');
foreach ($oDashletList as $oPosDashletNode) {
$aGridDashlet = [];
$aGridDashlet['position_x'] = intval($oPosDashletNode->GetChildText('position_x', '0'));
$aGridDashlet['position_y'] = intval($oPosDashletNode->GetChildText('position_y', '0'));
$aGridDashlet['width'] = intval($oPosDashletNode->GetChildText('width', '2'));
$aGridDashlet['height'] = intval($oPosDashletNode->GetChildText('height', '1'));
$oDashletNode = $oPosDashletNode->GetUniqueElement('dashlet');
$sId = $oPosDashletNode->getAttribute('id');
$oDashlet = $this->InitDashletFromDOMNode($oDashletNode);
$oDashlet->SetID($sId);
$aGridDashlet['dashlet'] = $oDashlet;
$this->aGridDashlets[] = $aGridDashlet;
}
}
/**
* @param DesignElement $oDomNode
*
* @return mixed
*/
@@ -160,7 +209,7 @@ abstract class Dashboard
// Test if dashlet can be instantiated, otherwise (uninstalled, broken, ...) we display a placeholder
$sClass = static::GetDashletClassFromType($sDashletType);
/** @var \Dashlet $oNewDashlet */
$oNewDashlet = new $sClass($this->oMetaModel, $sId);
$oNewDashlet = $this->oDashletFactory->CreateDashlet($sClass, $sId);
$oNewDashlet->SetDashletType($sDashletType);
$oNewDashlet->FromDOMNode($oDomNode);
@@ -204,20 +253,33 @@ abstract class Dashboard
*/
public function ToXml()
{
$oDoc = new DOMDocument();
$oDoc->formatOutput = true; // indent (must be loaded with option LIBXML_NOBLANKS)
$oDoc->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect
$oMainNode = $oDoc->createElement('dashboard');
$oMainNode->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance");
$oDoc->appendChild($oMainNode);
$oMainNode = $this->CreateEmptyDashboard();
$this->ToDOMNode($oMainNode);
$sXml = $oDoc->saveXML();
$sXml = $oMainNode->ownerDocument->saveXML();
return $sXml;
}
/**
* @return DesignElement
* @throws \DOMException
*/
public function CreateEmptyDashboard(): DesignElement
{
$oDoc = new DesignDocument();
$oDoc->formatOutput = true; // indent (must be loaded with option LIBXML_NOBLANKS)
$oDoc->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect
/** @var DesignElement $oMainNode */
$oMainNode = $oDoc->createElement('dashboard');
$oMainNode->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
$oDoc->appendChild($oMainNode);
return $oMainNode;
}
/**
* @param \DOMElement $oDefinition
*/
@@ -279,7 +341,7 @@ abstract class Dashboard
}
$this->sTitle = $aParams['title'];
$this->bAutoReload = $aParams['auto_reload'] == 'true';
$this->iAutoReloadSec = max(MetaModel::GetConfig()->Get('min_reload_interval'), (int) $aParams['auto_reload_sec']);
$this->iAutoReloadSec = max(MetaModel::GetConfig()->Get('min_reload_interval'), (int)$aParams['auto_reload_sec']);
foreach ($aParams['cells'] as $aCell) {
$aCellDashlets = [];
@@ -287,7 +349,7 @@ abstract class Dashboard
$sDashletClass = $aDashletParams['dashlet_class'];
$sId = $aDashletParams['dashlet_id'];
/** @var \Dashlet $oNewDashlet */
$oNewDashlet = new $sDashletClass($this->oMetaModel, $sId);
$oNewDashlet = $this->oDashletFactory->CreateDashlet($sDashletClass, $sId);
if (isset($aDashletParams['dashlet_type'])) {
$oNewDashlet->SetDashletType($aDashletParams['dashlet_type']);
}
@@ -305,7 +367,11 @@ abstract class Dashboard
public function Save()
{
}
public function PersistDashboard(string $sXml): bool
{
return true;
}
/**
@@ -503,7 +569,7 @@ EOF
{
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? null, $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
/** @var \DashboardLayoutMultiCol $oLayout */
/** @var \DashboardLayout $oLayout */
$oLayout = new $this->sLayoutClass();
foreach ($this->aCells as $iCellIdx => $aDashlets) {
@@ -513,7 +579,13 @@ EOF
}
}
$oDashboard = $oLayout->Render($oPage, $this->aCells, $bEditMode, $aExtraParams);
if (count($this->aCells) > 0) {
$aDashlets = $this->aCells;
} else {
$aDashlets = $this->aGridDashlets;
}
$oDashboard = $oLayout->Render($oPage, $aDashlets, $bEditMode, $aExtraParams);
$oPage->AddUiBlock($oDashboard);
$bFromDasboardPage = isset($aExtraParams['from_dashboard_page']) ? isset($aExtraParams['from_dashboard_page']) : false;
@@ -603,29 +675,12 @@ JS
* Return an array of dashlets available for selection.
*
* @return array
* @throws \ReflectionException
* @throws \Combodo\iTop\Application\Dashlet\DashletException
* @throws \DOMFormatException
*/
protected function GetAvailableDashlets()
protected function GetAvailableDashlets(): array
{
$aDashlets = [];
foreach (get_declared_classes() as $sDashletClass) {
// DashletUnknown is not among the selection as it is just a fallback for dashlets that can't instantiated.
if (is_subclass_of($sDashletClass, 'Dashlet') && !in_array($sDashletClass, ['DashletUnknown', 'DashletProxy'])) {
$oReflection = new ReflectionClass($sDashletClass);
if (!$oReflection->isAbstract()) {
$aCallSpec = [$sDashletClass, 'IsVisible'];
$bVisible = call_user_func($aCallSpec);
if ($bVisible) {
$aCallSpec = [$sDashletClass, 'GetInfo'];
$aInfo = call_user_func($aCallSpec);
$aDashlets[$sDashletClass] = $aInfo;
}
}
}
}
return $aDashlets;
return DashletService::GetInstance()->GetAvailableDashlets();
}
/**
@@ -640,6 +695,7 @@ JS
$iNewId = max($iNewId, (int)$oDashlet->GetID());
}
}
return $iNewId + 1;
}
@@ -674,6 +730,7 @@ JS
if (is_subclass_of($sType, 'Dashlet')) {
return $sType;
}
return 'DashletUnknown';
}
@@ -726,6 +783,8 @@ class RuntimeDashboard extends Dashboard
{
parent::__construct($sId);
$this->oMetaModel = new ModelReflectionRuntime();
$this->oDashletFactory->SetModelReflectionRuntime($this->oMetaModel);
ServiceLocator::GetInstance()->RegisterService('ModelReflection', $this->oMetaModel);
$this->bCustomized = false;
}
@@ -740,6 +799,7 @@ class RuntimeDashboard extends Dashboard
/**
* @param bool $bCustomized
*
* @since 2.7.0
*/
public function SetCustomFlag($bCustomized)
@@ -764,6 +824,26 @@ class RuntimeDashboard extends Dashboard
public function Save()
{
$sXml = $this->ToXml();
return $this->PersistDashboard($sXml);
}
/**
* @param string $sXml
*
* @return bool
* @throws \ArchivedObjectException
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \CoreWarning
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \OQLException
*/
public function PersistDashboard(string $sXml): bool
{
$oUDSearch = new DBObjectSearch('UserDashboard');
$oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
$oUDSearch->AddCondition('menu_code', $this->sId, '=');
@@ -784,6 +864,7 @@ class RuntimeDashboard extends Dashboard
utils::PushArchiveMode(false);
$oUserDashboard->DBWrite();
utils::PopArchiveMode();
return $bIsNew;
}
@@ -1077,6 +1158,7 @@ JS
$sId = utils::Sanitize($this->GetId(), '', 'element_identifier');
$sMenuTogglerId = "ibo-dashboard-menu-toggler-{$sId}";
$sActionEditId = "ibo-dashboard-menu-edit-{$sId}";
$sPopoverMenuId = "ibo-dashboard-menu-popover-{$sId}";
$sName = 'UI:Dashboard:Actions';
@@ -1090,6 +1172,21 @@ JS
} else {
$oToolbar = $oDashboard->GetToolbar();
}
// TODO 3.3 Check if we need different action for custom dashboard creation / edition
$oActionEditButton = ButtonUIBlockFactory::MakeIconAction(
'fas fa-pen',
$this->HasCustomDashboard() ? Dict::S('UI:Dashboard:EditCustom') : Dict::S('UI:Dashboard:CreateCustom'),
$sActionEditId,
'',
false,
$sActionEditId
)
->AddCSSClass('ibo-top-bar--toolbar-dashboard-edit-button')
->AddCSSClass('ibo-action-button');
$oToolbar->AddSubBlock($oActionEditButton);
$oActionButton = ButtonUIBlockFactory::MakeIconAction('fas fa-ellipsis-v', Dict::S($sName), $sName, '', false, $sMenuTogglerId)
->AddCSSClass('ibo-top-bar--toolbar-dashboard-menu-toggler')
->AddCSSClass('ibo-action-button');
@@ -1099,8 +1196,8 @@ JS
$sFile = addslashes(utils::LocalPath($this->sDefinitionFile));
$sJSExtraParams = json_encode($aExtraParams);
if ($this->HasCustomDashboard()) {
$oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:EditCustom'), "return EditDashboard('{$this->sId}', '$sFile', $sJSExtraParams)");
$aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
// $oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:EditCustom'), "return EditDashboard('{$this->sId}', '$sFile', $sJSExtraParams)");
// $aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
$oRevert = new JSPopupMenuItem(
'UI:Dashboard:RevertConfirm',
Dict::S('UI:Dashboard:DeleteCustom'),
@@ -1108,8 +1205,8 @@ JS
);
$aActions[$oRevert->GetUID()] = $oRevert->GetMenuItem();
} else {
$oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:CreateCustom'), "return EditDashboard('{$this->sId}', '$sFile', $sJSExtraParams)");
$aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
// $oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:CreateCustom'), "return EditDashboard('{$this->sId}', '$sFile', $sJSExtraParams)");
// $aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
}
utils::GetPopupMenuItems($oPage, iPopupMenuExtension::MENU_DASHBOARD_ACTIONS, $this, $aActions);
@@ -1223,7 +1320,7 @@ EOF
$sId = json_encode($this->sId);
$sLayoutClass = json_encode($this->sLayoutClass);
$sAutoReload = $this->bAutoReload ? 'true' : 'false';
$sAutoReloadSec = (string) $this->iAutoReloadSec;
$sAutoReloadSec = (string)$this->iAutoReloadSec;
$sTitle = json_encode($this->sTitle);
$sFile = json_encode($this->GetDefinitionFile());
$sUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
@@ -1395,19 +1492,11 @@ JS
// Get the list of possible dashlets that support a creation from
// an OQL
$aAllDashlets = DashletService::GetInstance()->GetAvailableDashlets();
$aDashlets = [];
foreach (get_declared_classes() as $sDashletClass) {
if (is_subclass_of($sDashletClass, 'Dashlet')) {
$oReflection = new ReflectionClass($sDashletClass);
if (!$oReflection->isAbstract()) {
$aCallSpec = [$sDashletClass, 'CanCreateFromOQL'];
$bShorcutMode = call_user_func($aCallSpec);
if ($bShorcutMode) {
$aCallSpec = [$sDashletClass, 'GetInfo'];
$aInfo = call_user_func($aCallSpec);
$aDashlets[$sDashletClass] = ['label' => $aInfo['label'], 'class' => $sDashletClass, 'icon' => $aInfo['icon']];
}
}
foreach ($aAllDashlets as $sDashletClass => $aInfo) {
if ($aInfo['can_create_by_oql']) {
$aDashlets[$sDashletClass] = ['label' => $aInfo['label'], 'class' => $sDashletClass, 'icon' => $aInfo['icon']];
}
}
@@ -1417,7 +1506,7 @@ JS
$oSubForm = new DesignerForm();
$oMetaModel = new ModelReflectionRuntime();
/** @var \Dashlet $oDashlet */
$oDashlet = new $sDashletClass($oMetaModel, 0);
$oDashlet = DashletFactory::GetInstance()->CreateDashlet($sDashletClass, 0);
$oDashlet->GetPropertiesFieldsFromOQL($oSubForm, $sOQL);
$oSelectorField->AddSubForm($oSubForm, $aDashletInfo['label'], $aDashletInfo['class']);
@@ -1571,7 +1660,7 @@ JS
*/
private function UpdateDashletUserPrefs(Dashlet $oDashlet, $sDashletIdOrig, array $aExtraParams)
{
$bIsDashletWithListPref = ($oDashlet instanceof DashletObjectList);
$bIsDashletWithListPref = ($oDashlet instanceof DashletObjectList);
if (!$bIsDashletWithListPref) {
return;
}
@@ -1619,6 +1708,7 @@ JS
//on error, return default value
return null;
}
return DataTableSettings::GetAppUserPreferenceKey($aClassAliases, $sDataTableId);
}
}

View File

@@ -30,7 +30,7 @@ use Combodo\iTop\Application\WebPage\WebPage;
*/
abstract class DashboardLayout
{
abstract public function Render($oPage, $aDashlets, $bEditMode = false);
abstract public function Render($oPage, $aDashlets, $bEditMode = false, array $aExtraParams = []);
/**
* @param int $iCellIdx
@@ -43,8 +43,8 @@ abstract class DashboardLayout
public static function GetInfo()
{
return [
'label' => '',
'icon' => '',
'label' => '',
'icon' => '',
'description' => '',
];
}
@@ -74,6 +74,7 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
}
$idx++;
}
return $aDashlets;
}
@@ -94,6 +95,7 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
}
$idx++;
}
return $aCells;
}
@@ -109,7 +111,8 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
// Trim the list of cells to remove the invisible/empty ones at the end of the array
$aCells = $this->TrimCellsArray($aCells);
$oDashboardLayout = new DashboardLayoutUIBlock();
// TODO 3.3 Handle dashboard new format, convert old format if needed
$oDashboardLayout = new DashboardLayoutUIBlock($aExtraParams['dashboard_div_id']);
//$oPage->AddUiBlock($oDashboardLayout);
$iCellIdx = 0;
@@ -117,15 +120,16 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
//Js given by each dashlet to reload
$sJSReload = "";
$oDashboardGrid = new \Combodo\iTop\Application\UI\Base\Layout\Dashboard\DashboardGrid();
$oDashboardLayout->SetGrid($oDashboardGrid);
for ($iRows = 0; $iRows < $iNbRows; $iRows++) {
$oDashboardRow = new DashboardRow();
$oDashboardLayout->AddDashboardRow($oDashboardRow);
//$oDashboardLayout->AddDashboardRow($oDashboardRow);
for ($iCols = 0; $iCols < $this->iNbCols; $iCols++) {
$oDashboardColumn = new DashboardColumn($bEditMode);
$oDashboardColumn->SetCellIndex($iCellIdx);
$oDashboardRow->AddDashboardColumn($oDashboardColumn);
//$oDashboardRow->AddDashboardColumn($oDashboardColumn);
if (array_key_exists($iCellIdx, $aCells)) {
$aDashlets = $aCells[$iCellIdx];
@@ -133,6 +137,18 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
/** @var \Dashlet $oDashlet */
foreach ($aDashlets as $oDashlet) {
if ($oDashlet::IsVisible()) {
$sDashletId = $oDashlet->GetID();
$sDashletClass = get_class($oDashlet);
$aDashletDenormalizedProperties = $oDashlet->GetDenormalizedProperties();
// $aDashletsInfo = $sDashletClass::GetInfo();
//
// // TODO 3.3 Gather real position and height/width if any.
// // Also set minimal height/width
// $iPositionX = null;
// $iPositionY = null;
// $iWidth = array_key_exists('preferred_width', $aDashletsInfo) ? $aDashletsInfo['preferred_width'] : 1;
// $iHeight = array_key_exists('preferred_height', $aDashletsInfo) ? $aDashletsInfo['preferred_height'] : 1;
// $oDashboardGrid->AddDashlet($oDashlet->DoRender($oPage, $bEditMode, true /* bEnclosingDiv */, $aExtraParams), $sDashletId, $sDashletClass, $aDashletDenormalizedProperties, $iPositionX, $iPositionY, $iWidth, $iHeight);
$oDashboardColumn->AddUIBlock($oDashlet->DoRender($oPage, $bEditMode, true /* bEnclosingDiv */, $aExtraParams));
}
}
@@ -147,6 +163,7 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
$sJSReload .= $oDashboardRow->GetJSRefreshCallback()." ";
}
// TODO 3.3 We can probably do better with the new dashboard
$oPage->add_script("function updateDashboard".$aExtraParams['dashboard_div_id']."(){".$sJSReload."}");
if ($bEditMode) { // Add one row for extensibility
@@ -168,8 +185,8 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
*/
public function GetDashletCoordinates($iCellIdx)
{
$iColNumber = (int) $iCellIdx % $this->iNbCols;
$iRowNumber = (int) floor($iCellIdx / $this->iNbCols);
$iColNumber = (int)$iCellIdx % $this->iNbCols;
$iRowNumber = (int)floor($iCellIdx / $this->iNbCols);
return [$iColNumber, $iRowNumber];
}
@@ -182,11 +199,12 @@ class DashboardLayoutOneCol extends DashboardLayoutMultiCol
parent::__construct();
$this->iNbCols = 1;
}
public static function GetInfo()
{
return [
'label' => 'One Column',
'icon' => 'images/layout_1col.png',
'label' => 'One Column',
'icon' => 'images/layout_1col.png',
'description' => '',
];
}
@@ -199,11 +217,12 @@ class DashboardLayoutTwoCols extends DashboardLayoutMultiCol
parent::__construct();
$this->iNbCols = 2;
}
public static function GetInfo()
{
return [
'label' => 'Two Columns',
'icon' => 'images/layout_2col.png',
'label' => 'Two Columns',
'icon' => 'images/layout_2col.png',
'description' => '',
];
}
@@ -216,11 +235,12 @@ class DashboardLayoutThreeCols extends DashboardLayoutMultiCol
parent::__construct();
$this->iNbCols = 3;
}
public static function GetInfo()
{
return [
'label' => 'Two Columns',
'icon' => 'images/layout_3col.png',
'label' => 'Two Columns',
'icon' => 'images/layout_3col.png',
'description' => '',
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.3">
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://www.combodo.com/itop-schema/3.3"
version="3.3">
<classes>
<class id="AbstractResource" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<comment>/* Resource access control abstraction. Can be herited by abstract resource access control classes. Generaly controlled using UR_ACTION_MODIFY access right. */</comment>
<comment>/* Resource access control abstraction. Can be inherited by abstract resource access control classes. Generally controlled using UR_ACTION_MODIFY access right. */</comment>
<abstract>true</abstract>
</properties>
<presentation/>
@@ -552,7 +554,7 @@ Call $this->AddInitialAttributeFlags($sAttCode, $iFlags) for all the initial att
<description><![CDATA[Inform the listeners about the connection states]]></description>
<event_data>
<event_datum id="code">
<description>The login step result code (LoginWebPage::EXIT_CODE_...) </description>
<description>The login step result code (LoginWebPage::EXIT_CODE_...)</description>
<type>integer</type>
</event_datum>
<event_datum id="state">
@@ -849,5 +851,454 @@ Call $this->AddInitialAttributeFlags($sAttCode, $iFlags) for all the initial att
</methods>
</class>
</classes>
<dashlets>
<dashlet id="DashletGroupByTable" _delta="define">
<label>UI:DashletGroupByTable:Label</label>
<icon>images/dashlets/icons8-transaction-list-48.png</icon>
<description>UI:DashletGroupByTable:Description</description>
<min_width>2</min_width>
<min_height>2</min_height>
<preferred_width>3</preferred_width>
<preferred_height>3</preferred_height>
<can_create_by_oql>true</can_create_by_oql>
<configuration/>
</dashlet>
<dashlet id="DashletGroupByBars" _delta="define">
<label>UI:DashletGroupByBars:Label</label>
<icon>images/dashlets/icons8-bar-chart-48.png</icon>
<description>UI:DashletGroupByBars:Description</description>
<min_width>2</min_width>
<min_height>2</min_height>
<preferred_width>3</preferred_width>
<preferred_height>3</preferred_height>
<can_create_by_oql>true</can_create_by_oql>
<configuration/>
</dashlet>
<dashlet id="DashletGroupByPie" _delta="define">
<label>UI:DashletGroupByPie:Label</label>
<icon>images/dashlets/icons8-pie-chart-48.png</icon>
<description>UI:DashletGroupByPie:Description</description>
<min_width>2</min_width>
<min_height>2</min_height>
<preferred_width>3</preferred_width>
<preferred_height>3</preferred_height>
<can_create_by_oql>true</can_create_by_oql>
<configuration/>
</dashlet>
<dashlet id="DashletBadge" _delta="define">
<label>UI:DashletBadge:Label</label>
<icon>images/dashlets/icons8-badge-48.png</icon>
<description>UI:DashletBadge:Description</description>
<min_width>2</min_width>
<min_height>1</min_height>
<preferred_width>2</preferred_width>
<preferred_height>1</preferred_height>
<configuration/>
</dashlet>
<dashlet id="DashletHeaderDynamic" _delta="define">
<label>UI:DashletHeaderDynamic:Label</label>
<icon>images/dashlets/icons8-header-altered-48.png</icon>
<description>UI:DashletHeaderDynamic:Description</description>
<min_width>2</min_width>
<min_height>1</min_height>
<preferred_width>4</preferred_width>
<preferred_height>3</preferred_height>
<configuration/>
</dashlet>
<dashlet id="DashletHeaderStatic" _delta="define">
<label>UI:DashletHeaderStatic:Label</label>
<icon>images/dashlets/icons8-header-48.png</icon>
<description>UI:DashletHeaderStatic:Description</description>
<min_width>4</min_width>
<min_height>1</min_height>
<preferred_width>4</preferred_width>
<preferred_height>1</preferred_height>
<configuration/>
</dashlet>
<dashlet id="DashletObjectList" _delta="define">
<label>UI:DashletObjectList:Label</label>
<icon>images/dashlets/icons8-list-48.png</icon>
<description>UI:DashletObjectList:Description</description>
<min_width>2</min_width>
<min_height>1</min_height>
<preferred_width>4</preferred_width>
<preferred_height>3</preferred_height>
<can_create_by_oql>true</can_create_by_oql>
<configuration/>
</dashlet>
<dashlet id="DashletPlainText" _delta="define">
<label>UI:DashletPlainText:Label</label>
<icon>images/dashlets/icons8-text-box-48.png</icon>
<description>UI:DashletPlainText:Description</description>
<min_width>2</min_width>
<min_height>1</min_height>
<preferred_width>2</preferred_width>
<preferred_height>1</preferred_height>
<configuration/>
</dashlet>
</dashlets>
<property_types _delta="define">
<property_type id="Dashboard" xsi:type="Combodo-AbstractPropertyType"/>
<property_type id="DashboardGrid" xsi:type="Combodo-PropertyType">
<extends>Dashboard</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<label>Dashboard</label>
<nodes>
<node id="title" xsi:type="Combodo-ValueType-String">
<label>UI:DashboardEdit:DashboardTitle</label>
</node>
<node id="refresh" xsi:type="Combodo-ValueType-Choice"> <!-- Possible de le cacher, etc celui-ci nous met dedans -->
<label>UI:DashboardEdit:AutoReload</label>
<values>
<value id="0">
<label>No auto-refresh</label>
</value>
<value id="30">
<label>Every 30 seconds</label>
</value>
<value id="60">
<label>Every 1 minute</label>
</value>
<value id="300">
<label>Every 5 minutes</label>
</value>
<value id="600">
<label>Every 10 minutes</label>
</value>
<value id="1800">
<label>Every 30 minutes</label>
</value>
<value id="3600">
<label>Every 1 hour</label>
</value>
</values>
</node>
<node id="pos_dashlets" xsi:type="Combodo-ValueType-Collection">
<label>Dashlet List</label>
<xml-format xsi:type="Combodo-XMLFormat-CollectionWithId">
<tag-name>pos_dashlet</tag-name>
</xml-format>
<prototype>
<node id="position_x" xsi:type="Combodo-ValueType-Integer">
<label>X</label>
</node>
<node id="position_y" xsi:type="Combodo-ValueType-Integer">
<label>Y</label>
</node>
<node id="width" xsi:type="Combodo-ValueType-Integer">
<label>W</label>
</node>
<node id="height" xsi:type="Combodo-ValueType-Integer">
<label>H</label>
</node>
<node id="dashlet" xsi:type="Combodo-ValueType-Polymorphic">
<label>Dashlet</label>
<allowed-types>
<allowed-type>Dashlet</allowed-type>
</allowed-types>
</node>
</prototype>
</node>
</nodes>
</definition>
</property_type>
<property_type id="Dashlet" xsi:type="Combodo-AbstractPropertyType"/>
<property_type id="DashletGroupByTable" xsi:type="Combodo-PropertyType">
<extends>Dashlet</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<label>UI:DashletGroupBy:Title</label>
<nodes>
<node id="title" xsi:type="Combodo-ValueType-Label">
<label>UI:DashletGroupBy:Prop-Title</label>
</node>
<node id="query" xsi:type="Combodo-ValueType-OQL">
<label>UI:DashletGroupBy:Prop-Query</label>
</node>
<node id="group_by" xsi:type="Combodo-ValueType-ClassAttributeGroupBy">
<label>UI:DashletGroupBy:Prop-GroupBy</label>
<class>{{query.selected_class}}</class>
</node>
<node id="style" xsi:type="Combodo-ValueType-Choice"> <!-- Possible de le cacher, etc celui-ci nous met dedans -->
<label>UI:DashletGroupBy:Prop-Style</label>
<values>
<value id="bars">
<label>UI:DashletGroupByBars:Label</label>
</value>
<value id="pie">
<label>UI:DashletGroupByPie:Label</label>
</value>
<value id="table">
<label>UI:DashletGroupByTable:Label</label>
</value>
</values>
</node>
<node id="aggregation_function" xsi:type="Combodo-ValueType-AggregateFunction">
<label>UI:DashletGroupBy:Prop-Function</label>
<class>{{query.selected_class}}</class> <!-- pour savoir si il y a des attributs additionnables -->
</node>
<node id="aggregation_attribute" xsi:type="Combodo-ValueType-ClassAttribute">
<label>UI:DashletGroupBy:Prop-FunctionAttribute</label>
<relevance-condition>{{aggregation_function.value != 'count'}}</relevance-condition>
<class>{{query.selected_class}}</class>
<category>numeric</category>
</node>
<node id="order_by" xsi:type="Combodo-ValueType-ChoiceFromInput">
<label>UI:DashletGroupBy:Prop-OrderField</label>
<values>
<value id="attribute">
<label>{{aggregation_attribute.label}}</label>
</value>
<value id="function">
<label>{{aggregation_function.label}}</label>
</value>
</values>
</node>
<node id="limit" xsi:type="Combodo-ValueType-Integer">
<label>UI:DashletGroupBy:Prop-Limit</label>
<relevance-condition>{{order_by.value = 'function'}}</relevance-condition>
</node>
<node id="order_direction" xsi:type="Combodo-ValueType-Choice">
<label>UI:DashletGroupBy:Prop-OrderDirection</label>
<values>
<value id="asc">
<label>UI:DashletGroupBy:Order:asc</label>
</value>
<value id="desc">
<label>UI:DashletGroupBy:Order:desc</label>
</value>
</values>
</node>
</nodes>
</definition>
</property_type>
<property_type id="DashletGroupByBars" xsi:type="Combodo-PropertyType">
<extends>Dashlet</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<label>UI:DashletGroupBy:Title</label>
<nodes>
<node id="title" xsi:type="Combodo-ValueType-Label">
<label>UI:DashletGroupBy:Prop-Title</label>
</node>
<node id="query" xsi:type="Combodo-ValueType-OQL">
<label>UI:DashletGroupBy:Prop-Query</label>
</node>
<node id="group_by" xsi:type="Combodo-ValueType-ClassAttributeGroupBy">
<label>UI:DashletGroupBy:Prop-GroupBy</label>
<class>{{query.selected_class}}</class>
</node>
<node id="style" xsi:type="Combodo-ValueType-Choice"> <!-- Possible de le cacher, etc celui-ci nous met dedans -->
<label>UI:DashletGroupBy:Prop-Style</label>
<values>
<value id="bars">
<label>UI:DashletGroupByBars:Label</label>
</value>
<value id="pie">
<label>UI:DashletGroupByPie:Label</label>
</value>
<value id="table">
<label>UI:DashletGroupByTable:Label</label>
</value>
</values>
</node>
<node id="aggregation_function" xsi:type="Combodo-ValueType-AggregateFunction">
<label>UI:DashletGroupBy:Prop-Function</label>
<class>{{query.selected_class}}</class> <!-- pour savoir si il y a des attributs additionnables -->
</node>
<node id="aggregation_attribute" xsi:type="Combodo-ValueType-ClassAttribute">
<label>UI:DashletGroupBy:Prop-FunctionAttribute</label>
<relevance-condition>{{aggregation_function.value != 'count'}}</relevance-condition>
<class>{{query.selected_class}}</class>
<category>numeric</category>
</node>
<node id="order_by" xsi:type="Combodo-ValueType-ChoiceFromInput">
<label>UI:DashletGroupBy:Prop-OrderField</label>
<values>
<value id="attribute">
<label>{{aggregation_attribute.label}}</label>
</value>
<value id="function">
<label>{{aggregation_function.label}}</label>
</value>
</values>
</node>
<node id="limit" xsi:type="Combodo-ValueType-Integer">
<label>UI:DashletGroupBy:Prop-Limit</label>
<relevance-condition>{{order_by.value = 'function'}}</relevance-condition>
</node>
<node id="order_direction" xsi:type="Combodo-ValueType-Choice">
<label>UI:DashletGroupBy:Prop-OrderDirection</label>
<values>
<value id="asc">
<label>UI:DashletGroupBy:Order:asc</label>
</value>
<value id="desc">
<label>UI:DashletGroupBy:Order:desc</label>
</value>
</values>
</node>
</nodes>
</definition>
</property_type>
<property_type id="DashletGroupByPie" xsi:type="Combodo-PropertyType">
<extends>Dashlet</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<label>UI:DashletGroupBy:Title</label>
<nodes>
<node id="title" xsi:type="Combodo-ValueType-Label">
<label>UI:DashletGroupBy:Prop-Title</label>
</node>
<node id="query" xsi:type="Combodo-ValueType-OQL">
<label>UI:DashletGroupBy:Prop-Query</label>
</node>
<node id="group_by" xsi:type="Combodo-ValueType-ClassAttributeGroupBy">
<label>UI:DashletGroupBy:Prop-GroupBy</label>
<class>{{query.selected_class}}</class>
</node>
<node id="style" xsi:type="Combodo-ValueType-Choice"> <!-- Possible de le cacher, etc celui-ci nous met dedans -->
<label>UI:DashletGroupBy:Prop-Style</label>
<values>
<value id="bars">
<label>UI:DashletGroupByBars:Label</label>
</value>
<value id="pie">
<label>UI:DashletGroupByPie:Label</label>
</value>
<value id="table">
<label>UI:DashletGroupByTable:Label</label>
</value>
</values>
</node>
<node id="aggregation_function" xsi:type="Combodo-ValueType-AggregateFunction">
<label>UI:DashletGroupBy:Prop-Function</label>
<class>{{query.selected_class}}</class> <!-- pour savoir si il y a des attributs additionnables -->
</node>
<node id="aggregation_attribute" xsi:type="Combodo-ValueType-ClassAttribute">
<label>UI:DashletGroupBy:Prop-FunctionAttribute</label>
<relevance-condition>{{aggregation_function.value != 'count'}}</relevance-condition>
<class>{{query.selected_class}}</class>
<category>numeric</category>
</node>
<node id="order_by" xsi:type="Combodo-ValueType-ChoiceFromInput">
<label>UI:DashletGroupBy:Prop-OrderField</label>
<values>
<value id="attribute">
<label>{{aggregation_attribute.label}}</label>
</value>
<value id="function">
<label>{{aggregation_function.label}}</label>
</value>
</values>
</node>
<node id="limit" xsi:type="Combodo-ValueType-Integer">
<label>UI:DashletGroupBy:Prop-Limit</label>
<relevance-condition>{{order_by.value = 'function'}}</relevance-condition>
</node>
<node id="order_direction" xsi:type="Combodo-ValueType-Choice">
<label>UI:DashletGroupBy:Prop-OrderDirection</label>
<values>
<value id="asc">
<label>UI:DashletGroupBy:Order:asc</label>
</value>
<value id="desc">
<label>UI:DashletGroupBy:Order:desc</label>
</value>
</values>
</node>
</nodes>
</definition>
</property_type>
<property_type id="DashletBadge" xsi:type="Combodo-PropertyType">
<extends>Dashlet</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<nodes>
<node id="class" xsi:type="Combodo-ValueType-Class">
<label>UI:DashletBadge:Prop-Class</label>
<categories-csv>bizmodel</categories-csv>
</node>
</nodes>
</definition>
</property_type>
<property_type id="DashletHeaderDynamic" xsi:type="Combodo-PropertyType">
<extends>Dashlet</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<label>UI:DashletHeaderDynamic:Title</label>
<nodes>
<node id="title" xsi:type="Combodo-ValueType-Label">
<label>UI:DashletHeaderDynamic:Prop-Title</label>
</node>
<node id="icon" xsi:type="Combodo-ValueType-Icon">
<label>UI:DashletHeaderDynamic:Prop-Icon</label>
</node>
<node id="subtitle" xsi:type="Combodo-ValueType-Label">
<label>UI:DashletHeaderDynamic:Prop-Subtitle</label>
</node>
<node id="query" xsi:type="Combodo-ValueType-OQL">
<label>UI:DashletHeaderDynamic:Prop-Query</label>
</node>
<node id="group_by" xsi:type="Combodo-ValueType-ClassAttribute">
<label>UI:DashletHeaderDynamic:Prop-GroupBy</label>
<class>{{query.selected_class}}</class>
<category>enum</category>
</node>
<node id="values" xsi:type="Combodo-ValueType-CollectionOfValues">
<label>UI:DashletHeaderDynamic:Prop-Values</label>
<xml-format xsi:type="Combodo-XMLFormat-CSV"/>
<value-type xsi:type="Combodo-ValueType-ClassAttributeValue">
<class>{{query.selected_class}}</class>
<attribute>{{group_by.attribute}}</attribute>
</value-type>
</node>
</nodes>
</definition>
</property_type>
<property_type id="DashletHeaderStatic" xsi:type="Combodo-PropertyType">
<extends>Dashlet</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<nodes>
<node id="title" xsi:type="Combodo-ValueType-Label">
<label>UI:DashletHeaderStatic:Prop-Title</label>
</node>
<node id="icon" xsi:type="Combodo-ValueType-Icon">
<label>UI:DashletHeaderStatic:Prop-Icon</label>
</node>
</nodes>
</definition>
</property_type>
<property_type id="DashletObjectList" xsi:type="Combodo-PropertyType">
<extends>Dashlet</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<nodes>
<node id="title" xsi:type="Combodo-ValueType-Label">
<label>UI:DashletObjectList:Prop-Title</label>
</node>
<node id="query" xsi:type="Combodo-ValueType-OQL">
<label>UI:DashletObjectList:Prop-Query</label>
</node>
<node id="menu" xsi:type="Combodo-ValueType-Boolean">
<label>UI:DashletObjectList:Prop-Menu</label>
<on>
<!-- not so cute, but matches exactly 3.2 implementation of boolean fields -->
<label>UI:UserManagement:ActionAllowed:Yes</label>
<value>true</value>
</on>
<off>
<label>UI:UserManagement:ActionAllowed:No</label>
<value>false</value>
</off>
</node>
</nodes>
</definition>
</property_type>
<property_type id="DashletPlainText" xsi:type="Combodo-PropertyType">
<extends>Dashlet</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<nodes>
<node id="text" xsi:type="Combodo-ValueType-Text">
<label>UI:DashletPlainText:Prop-Text</label>
</node>
</nodes>
</definition>
</property_type>
</property_types>
</meta>
</itop_design>

View File

@@ -6,6 +6,7 @@
*/
use Combodo\iTop\Application\Helper\WebResourcesHelper;
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
use Combodo\iTop\Application\WebPage\ErrorPage;
use Combodo\iTop\Application\WebPage\iTopWebPage;
use Combodo\iTop\Application\WebPage\WebPage;
@@ -1279,13 +1280,14 @@ class DashboardMenuNode extends MenuNode
if ($oDashboard != null) {
WebResourcesHelper::EnableC3JSToWebPage($oPage);
// TODO 3.3 this works for dashboard menu, what about other places ?
$oPageLayout = PageContentFactory::MakeForDashboard();
$oPage->SetContentLayout($oPageLayout, $oPage);
$sDivId = utils::Sanitize($this->sMenuId, '', 'element_identifier');
$oPage->add('<div id="'.$sDivId.'" class="ibo-dashboard" data-role="ibo-dashboard">');
$aExtraParams['dashboard_div_id'] = $sDivId;
$aExtraParams['from_dashboard_page'] = true;
$oDashboard->SetReloadURL($this->GetHyperlink($aExtraParams));
$oDashboard->Render($oPage, false, $aExtraParams);
$oPage->add('</div>');
$bEdit = utils::ReadParam('edit', false);
if ($bEdit) {

View File

@@ -1900,6 +1900,12 @@ SQL;
return $response;
}
public static function QuoteForPHP(string $sValue): string
{
$sEscaped = str_replace(['\\', "'"], ['\\\\', "\\'"], $sValue);
return "'$sEscaped'";
}
/**
* Get a standard list of character sets
*

View File

@@ -31,6 +31,7 @@
"symfony/mailer": "^6.4",
"symfony/security-csrf": "^6.4",
"symfony/twig-bundle": "~6.4.0",
"symfony/validator" : "^6.4",
"symfony/yaml": "~6.4.0",
"tecnickcom/tcpdf": "^6.6.0",
"thenetworg/oauth2-azure": "^2.0"

103
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "26fa7aa920057d080bcc0948bf052fda",
"content-hash" : "3e627286597661542dd598499c2bcc36",
"packages": [
{
"name": "apereo/phpcas",
@@ -4984,6 +4984,107 @@
"time": "2025-07-10T08:14:14+00:00"
},
{
"name" : "symfony/validator",
"version" : "v6.4.29",
"source" : {
"type" : "git",
"url" : "https://github.com/symfony/validator.git",
"reference" : "99df8a769e64e399f510166141ea74f450e8dd1d"
},
"dist" : {
"type" : "zip",
"url" : "https://api.github.com/repos/symfony/validator/zipball/99df8a769e64e399f510166141ea74f450e8dd1d",
"reference" : "99df8a769e64e399f510166141ea74f450e8dd1d",
"shasum" : ""
},
"require" : {
"php" : ">=8.1",
"symfony/deprecation-contracts" : "^2.5|^3",
"symfony/polyfill-ctype" : "~1.8",
"symfony/polyfill-mbstring" : "~1.0",
"symfony/polyfill-php83" : "^1.27",
"symfony/translation-contracts" : "^2.5|^3"
},
"conflict" : {
"doctrine/annotations" : "<1.13",
"doctrine/lexer" : "<1.1",
"symfony/dependency-injection" : "<5.4",
"symfony/expression-language" : "<5.4",
"symfony/http-kernel" : "<5.4",
"symfony/intl" : "<5.4",
"symfony/property-info" : "<5.4",
"symfony/translation" : "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3",
"symfony/yaml" : "<5.4"
},
"require-dev" : {
"doctrine/annotations" : "^1.13|^2",
"egulias/email-validator" : "^2.1.10|^3|^4",
"symfony/cache" : "^5.4|^6.0|^7.0",
"symfony/config" : "^5.4|^6.0|^7.0",
"symfony/console" : "^5.4|^6.0|^7.0",
"symfony/dependency-injection" : "^5.4|^6.0|^7.0",
"symfony/expression-language" : "^5.4|^6.0|^7.0",
"symfony/finder" : "^5.4|^6.0|^7.0",
"symfony/http-client" : "^5.4|^6.0|^7.0",
"symfony/http-foundation" : "^5.4|^6.0|^7.0",
"symfony/http-kernel" : "^5.4|^6.0|^7.0",
"symfony/intl" : "^5.4|^6.0|^7.0",
"symfony/mime" : "^5.4|^6.0|^7.0",
"symfony/property-access" : "^5.4|^6.0|^7.0",
"symfony/property-info" : "^5.4|^6.0|^7.0",
"symfony/translation" : "^5.4.35|~6.3.12|^6.4.3|^7.0.3",
"symfony/yaml" : "^5.4|^6.0|^7.0"
},
"type" : "library",
"autoload" : {
"psr-4" : {
"Symfony\\Component\\Validator\\" : ""
},
"exclude-from-classmap" : [
"/Tests/",
"/Resources/bin/"
]
},
"notification-url" : "https://packagist.org/downloads/",
"license" : [
"MIT"
],
"authors" : [
{
"name" : "Fabien Potencier",
"email" : "fabien@symfony.com"
},
{
"name" : "Symfony Community",
"homepage" : "https://symfony.com/contributors"
}
],
"description" : "Provides tools to validate values",
"homepage" : "https://symfony.com",
"support" : {
"source" : "https://github.com/symfony/validator/tree/v6.4.29"
},
"funding" : [
{
"url" : "https://symfony.com/sponsor",
"type" : "custom"
},
{
"url" : "https://github.com/fabpot",
"type" : "github"
},
{
"url" : "https://github.com/nicolas-grekas",
"type" : "github"
},
{
"url" : "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type" : "tidelift"
}
],
"time" : "2025-11-06T20:26:06+00:00"
},
{
"name": "symfony/var-dumper",
"version": "v6.4.26",
"source": {

View File

@@ -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');

View File

@@ -2685,13 +2685,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 +2740,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 +2758,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)) {

View File

@@ -5108,8 +5108,8 @@ abstract class DBObject implements iDisplay
protected function GetReferencingObjectsForDeletion($bAllowAllData = false)
{
$aDependentObjects = [];
$aReferencingMe = MetaModel::EnumReferencingClasses(get_class($this));
foreach ($aReferencingMe as $sRemoteClass => $aExtKeys) {
$aRererencingMe = MetaModel::EnumReferencingClasses(get_class($this));
foreach ($aRererencingMe as $sRemoteClass => $aExtKeys) {
/** @var \AttributeExternalKey $oExtKeyAttDef */
foreach ($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) {
// skip if external key doesn't require the deletion cascading

View File

@@ -41,7 +41,7 @@ use utils;
/**
* Class \Combodo\iTop\DesignDocument
*
* A design document is the DOM tree that modelize behaviors. One of its
* A design document is the DOM tree that models behaviors. One of its
* characteristics is that it can be altered by the mean of the same kind of document.
*
*/

View File

@@ -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
*

View File

@@ -55,6 +55,11 @@ abstract class ModelReflection
abstract public function GetFiltersList($sClass);
abstract public function IsValidFilterCode($sClass, $sFilterCode);
/**
* @since 3.3.0
*/
abstract public function IsAbstract($sClass): bool;
/**
* @param string $sOQL
*
@@ -84,9 +89,17 @@ abstract class ModelReflection
* @param string $defaultValue
*
* @return \RunTimeIconSelectionField
* @deprecated since 3.3.0 replaced by GetAvailableIcons
*/
abstract public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = '');
/**
* Find available icons for the current context
*
* @return array of ['value', 'label', 'icon'] where 'value' is the relative path on disk, 'label' the name to display and 'icon' is the URL to get the image
*/
abstract public function GetAvailableIcons(): array;
abstract public function GetRootClass($sClass);
abstract public function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP);
}
@@ -104,6 +117,8 @@ abstract class QueryReflection
class ModelReflectionRuntime extends ModelReflection
{
private static array $aAllIcons = [];
public function __construct()
{
}
@@ -148,7 +163,7 @@ class ModelReflectionRuntime extends ModelReflection
$sAttributeClass = get_class($oAttDef);
if ($aScope != null) {
foreach ($aScope as $sScopeClass) {
if (($sAttributeClass == $sScopeClass) || is_subclass_of($sAttributeClass, $sScopeClass)) {
if (is_a($sAttributeClass, $sScopeClass, true)) {
$aAttributes[$sAttCode] = $sAttributeClass;
break;
}
@@ -230,6 +245,11 @@ class ModelReflectionRuntime extends ModelReflection
return MetaModel::IsValidFilterCode($sClass, $sFilterCode);
}
public function IsAbstract($sClass): bool
{
return MetaModel::IsAbstract($sClass);
}
public function GetQuery($sOQL)
{
return new QueryReflectionRuntime($sOQL, $this);
@@ -245,6 +265,52 @@ class ModelReflectionRuntime extends ModelReflection
return new RunTimeIconSelectionField($sCode, $sLabel, $defaultValue);
}
public function GetAvailableIcons(): array
{
$aFolderList = [
APPROOT.'env-'.utils::GetCurrentEnvironment() => utils::GetAbsoluteUrlModulesRoot(),
APPROOT.'images/icons' => utils::GetAbsoluteUrlAppRoot().'images/icons',
];
if (count(self::$aAllIcons) == 0) {
foreach ($aFolderList as $sFolderPath => $sUrlPrefix) {
$aIcons = self::FindIconsOnDisk($sFolderPath);
ksort($aIcons);
foreach ($aIcons as $sFilePath) {
self::$aAllIcons[] = ['value' => $sFilePath, 'label' => basename($sFilePath), 'icon' => $sUrlPrefix.$sFilePath];
}
}
}
return self::$aAllIcons;
}
private static function FindIconsOnDisk(string $sBaseDir, string $sDir = '', array &$aFilesSpecs = []): array
{
$aResult = [];
// Populate automatically the list of icon files
if ($hDir = @opendir($sBaseDir.'/'.$sDir)) {
while (($sFile = readdir($hDir)) !== false) {
$aMatches = [];
if (($sFile != '.') && ($sFile != '..') && ($sFile != 'lifecycle') && is_dir($sBaseDir.'/'.$sDir.'/'.$sFile)) {
$sDirSubPath = ($sDir == '') ? $sFile : $sDir.'/'.$sFile;
$aResult = array_merge($aResult, self::FindIconsOnDisk($sBaseDir, $sDirSubPath, $aFilesSpecs));
}
$sSize = filesize($sBaseDir.'/'.$sDir.'/'.$sFile);
if (isset($aFilesSpecs[$sFile]) && $aFilesSpecs[$sFile] == $sSize) {
continue;
}
if (preg_match('/\.(png|jpg|jpeg|gif|svg)$/i', $sFile, $aMatches)) { // png, jp(e)g, gif and svg are considered valid
$aResult[$sFile.'_'.$sDir] = $sDir.'/'.$sFile;
$aFilesSpecs[$sFile] = $sSize;
}
}
closedir($hDir);
}
return $aResult;
}
public function GetRootClass($sClass)
{
return MetaModel::GetRootClass($sClass);

View File

@@ -2103,12 +2103,18 @@ class VariableExpression extends UnaryExpression
/**
* Evaluate the value of the expression
*
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
*
* @return mixed
* @throws \MissingQueryArgument
*/
public function Evaluate(array $aArgs)
{
throw new Exception('not implemented yet');
if (!isset($aArgs[$this->m_sName])) {
throw new MissingQueryArgument('Missing variable expression argument', array('expecting'=>$this->m_sName));
}
return $aArgs[$this->m_sName];
}
/**

View File

@@ -72,9 +72,15 @@ class OQLException extends CoreException
}
else
{
$sExpectations = '{'.implode(', ', $this->m_aExpecting).'}';
$sMessage = "$sIssue - found '{$this->m_sUnexpected}' at $iCol in '$sInput'";
if (count($this->m_aExpecting) < 30) {
$sExpectations = '{'.implode(', ', $this->m_aExpecting).'}';
$sMessage .= ', expecting '.json_encode($sExpectations);
}
$sSuggest = self::FindClosestString($this->m_sUnexpected, $this->m_aExpecting);
$sMessage = "$sIssue - found '{$this->m_sUnexpected}' at $iCol in '$sInput', expecting $sExpectations, I would suggest to use '$sSuggest'";
if (strlen($sSuggest) > 0) {
$sMessage .= ", I would suggest to use ".json_encode($sSuggest);
}
}
// make sure everything is assigned properly
@@ -155,5 +161,3 @@ class OQLException extends CoreException
return $sRet;
}
}
?>

View File

@@ -57,15 +57,15 @@ class OqlName
{
return $this->m_iPos;
}
public function __toString()
{
return $this->m_sValue;
}
}
}
/**
*
*
* Store hexadecimal values as strings so that we can support 64-bit values
*
*/
@@ -77,12 +77,12 @@ class OqlHexValue
{
$this->m_sValue = $sValue;
}
public function __toString()
{
return $this->m_sValue;
}
}
class OqlJoinSpec
@@ -109,6 +109,7 @@ class OqlJoinSpec
{
return $this->m_oClass->GetValue();
}
public function GetClassAlias()
{
return $this->m_oClassAlias->GetValue();
@@ -118,6 +119,7 @@ class OqlJoinSpec
{
return $this->m_oClass;
}
public function GetClassAliasDetails()
{
return $this->m_oClassAlias;
@@ -127,10 +129,12 @@ class OqlJoinSpec
{
return $this->m_oLeftField;
}
public function GetRightField()
{
return $this->m_oRightField;
}
public function GetOperator()
{
return $this->m_sOperator;
@@ -146,8 +150,9 @@ interface CheckableExpression
* @param ModelReflection $oModelReflection MetaModel to consider
* @param array $aAliases Aliases to class names (for the current query)
* @param string $sSourceQuery For the reporting
*
* @throws OqlNormalizeException
*/
*/
public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery);
}
@@ -168,13 +173,11 @@ class MatchOqlExpression extends MatchExpression implements CheckableExpression
$this->m_oRightExpr->Check($oModelReflection, $aAliases, $sSourceQuery);
// Only field MATCHES scalar is allowed
if (!$this->m_oLeftExpr instanceof FieldExpression)
{
if (!$this->m_oLeftExpr instanceof FieldExpression) {
throw new OqlNormalizeException('Only "field MATCHES string" syntax is allowed', $sSourceQuery, new OqlName($this->m_oLeftExpr->RenderExpression(true), 0));
}
// Only field MATCHES scalar is allowed
if (!$this->m_oRightExpr instanceof ScalarExpression && !$this->m_oRightExpr instanceof VariableOqlExpression)
{
if (!$this->m_oRightExpr instanceof ScalarExpression && !$this->m_oRightExpr instanceof VariableOqlExpression) {
throw new OqlNormalizeException('Only "field MATCHES string" syntax is allowed', $sSourceQuery, new OqlName($this->m_oRightExpr->RenderExpression(true), 0));
}
}
@@ -198,7 +201,7 @@ class NestedQueryOqlExpression extends NestedQueryExpression implements Checkabl
*
* @param OQLObjectQuery $oOQLObjectQuery
*/
public function __construct($oOQLObjectQuery )
public function __construct($oOQLObjectQuery)
{
parent::__construct($oOQLObjectQuery->ToDBSearch(""));
$this->m_oOQLObjectQuery = $oOQLObjectQuery;
@@ -232,8 +235,7 @@ class FieldOqlExpression extends FieldExpression implements CheckableExpression
public function __construct($oName, $oParent = null)
{
if (is_null($oParent))
{
if (is_null($oParent)) {
$oParent = new OqlName('', 0);
}
$this->m_oParent = $oParent;
@@ -256,37 +258,28 @@ class FieldOqlExpression extends FieldExpression implements CheckableExpression
{
$sClassAlias = $this->GetParent();
$sFltCode = $this->GetName();
if (empty($sClassAlias))
{
if (empty($sClassAlias)) {
// Try to find an alias
// Build an array of field => array of aliases
$aFieldClasses = array();
foreach($aAliases as $sAlias => $sReal)
{
foreach($oModelReflection->GetFiltersList($sReal) as $sAnFltCode)
{
foreach ($aAliases as $sAlias => $sReal) {
foreach ($oModelReflection->GetFiltersList($sReal) as $sAnFltCode) {
$aFieldClasses[$sAnFltCode][] = $sAlias;
}
}
if (!array_key_exists($sFltCode, $aFieldClasses))
{
if (!array_key_exists($sFltCode, $aFieldClasses)) {
throw new OqlNormalizeException('Unknown filter code', $sSourceQuery, $this->GetNameDetails(), array_keys($aFieldClasses));
}
if (count($aFieldClasses[$sFltCode]) > 1)
{
if (count($aFieldClasses[$sFltCode]) > 1) {
throw new OqlNormalizeException('Ambiguous filter code', $sSourceQuery, $this->GetNameDetails());
}
$sClassAlias = $aFieldClasses[$sFltCode][0];
}
else
{
if (!array_key_exists($sClassAlias, $aAliases))
{
} else {
if (!array_key_exists($sClassAlias, $aAliases)) {
throw new OqlNormalizeException('Unknown class [alias]', $sSourceQuery, $this->GetParentDetails(), array_keys($aAliases));
}
$sClass = $aAliases[$sClassAlias];
if (!$oModelReflection->IsValidFilterCode($sClass, $sFltCode))
{
if (!$oModelReflection->IsValidFilterCode($sClass, $sFltCode)) {
throw new OqlNormalizeException('Unknown filter code', $sSourceQuery, $this->GetNameDetails(), $oModelReflection->GetFiltersList($sClass));
}
}
@@ -305,8 +298,7 @@ class ListOqlExpression extends ListExpression implements CheckableExpression
{
public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
{
foreach ($this->GetItems() as $oItemExpression)
{
foreach ($this->GetItems() as $oItemExpression) {
$oItemExpression->Check($oModelReflection, $aAliases, $sSourceQuery);
}
}
@@ -316,8 +308,7 @@ class FunctionOqlExpression extends FunctionExpression implements CheckableExpre
{
public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
{
foreach ($this->GetArgs() as $oArgExpression)
{
foreach ($this->GetArgs() as $oArgExpression) {
$oArgExpression->Check($oModelReflection, $aAliases, $sSourceQuery);
}
}
@@ -350,6 +341,7 @@ abstract class OqlQuery
* Determine the class
*
* @param ModelReflection $oModelReflection MetaModel to consider
*
* @return string
* @throws Exception
*/
@@ -392,6 +384,7 @@ class OqlObjectQuery extends OqlQuery
* Determine the class
*
* @param ModelReflection $oModelReflection MetaModel to consider
*
* @return string
* @throws Exception
*/
@@ -415,6 +408,7 @@ class OqlObjectQuery extends OqlQuery
{
return $this->m_oClass;
}
public function GetClassAliasDetails()
{
return $this->m_oClassAlias;
@@ -424,6 +418,7 @@ class OqlObjectQuery extends OqlQuery
{
return $this->m_aJoins;
}
public function GetCondition()
{
return $this->m_oCondition;
@@ -432,44 +427,37 @@ class OqlObjectQuery extends OqlQuery
/**
* Recursively check the validity of the expression with regard to the data model
* and the query in which it is used
*
* @param ModelReflection $oModelReflection MetaModel to consider
*
* @param ModelReflection $oModelReflection MetaModel to consider
*
* @throws OqlNormalizeException
*/
*/
public function Check(ModelReflection $oModelReflection, $sSourceQuery, $aParentAliases = array())
{
$sClass = $this->GetClass($oModelReflection);
$sClassAlias = $this->GetClassAlias();
if (!$oModelReflection->IsValidClass($sClass))
{
if (!$oModelReflection->IsValidClass($sClass)) {
throw new UnknownClassOqlException($sSourceQuery, $this->GetClassDetails(), $oModelReflection->GetClasses());
}
$aAliases = array_merge(array($sClassAlias => $sClass),$aParentAliases);
$aAliases = array_merge(array($sClassAlias => $sClass), $aParentAliases);
$aJoinSpecs = $this->GetJoins();
if (is_array($aJoinSpecs))
{
foreach ($aJoinSpecs as $oJoinSpec)
{
if (is_array($aJoinSpecs)) {
foreach ($aJoinSpecs as $oJoinSpec) {
$sJoinClass = $oJoinSpec->GetClass();
$sJoinClassAlias = $oJoinSpec->GetClassAlias();
if (!$oModelReflection->IsValidClass($sJoinClass))
{
if (!$oModelReflection->IsValidClass($sJoinClass)) {
throw new UnknownClassOqlException($sSourceQuery, $oJoinSpec->GetClassDetails(), $oModelReflection->GetClasses());
}
if (array_key_exists($sJoinClassAlias, $aAliases))
{
if ($sJoinClassAlias != $sJoinClass)
{
if (array_key_exists($sJoinClassAlias, $aAliases)) {
if ($sJoinClassAlias != $sJoinClass) {
throw new OqlNormalizeException('Duplicate class alias', $sSourceQuery, $oJoinSpec->GetClassAliasDetails());
}
else
{
} else {
throw new OqlNormalizeException('Duplicate class name', $sSourceQuery, $oJoinSpec->GetClassDetails());
}
}
}
// Assumption: ext key on the left only !!!
// normalization should take care of this
@@ -480,85 +468,74 @@ class OqlObjectQuery extends OqlQuery
$oRightField = $oJoinSpec->GetRightField();
$sToClass = $oRightField->GetParent();
$sPKeyDescriptor = $oRightField->GetName();
if ($sPKeyDescriptor != 'id')
{
if ($sPKeyDescriptor != 'id') {
throw new OqlNormalizeException('Wrong format for Join clause (right hand), expecting an id', $sSourceQuery, $oRightField->GetNameDetails(), array('id'));
}
$aAliases[$sJoinClassAlias] = $sJoinClass;
if (!array_key_exists($sFromClass, $aAliases))
{
if (!array_key_exists($sFromClass, $aAliases)) {
throw new OqlNormalizeException('Unknown class in join condition (left expression)', $sSourceQuery, $oLeftField->GetParentDetails(), array_keys($aAliases));
}
if (!array_key_exists($sToClass, $aAliases))
{
if (!array_key_exists($sToClass, $aAliases)) {
throw new OqlNormalizeException('Unknown class in join condition (right expression)', $sSourceQuery, $oRightField->GetParentDetails(), array_keys($aAliases));
}
$aExtKeys = $oModelReflection->ListAttributes($aAliases[$sFromClass], \Combodo\iTop\Core\AttributeDefinition\AttributeExternalKey::class);
$aObjKeys = $oModelReflection->ListAttributes($aAliases[$sFromClass], \Combodo\iTop\Core\AttributeDefinition\AttributeObjectKey::class);
$aAllKeys = array_merge($aExtKeys, $aObjKeys);
if (!array_key_exists($sExtKeyAttCode, $aAllKeys))
{
if (!array_key_exists($sExtKeyAttCode, $aAllKeys)) {
throw new OqlNormalizeException('Unknown key in join condition (left expression)', $sSourceQuery, $oLeftField->GetNameDetails(), array_keys($aAllKeys));
}
if ($sFromClass == $sJoinClassAlias)
{
if ($sFromClass == $sJoinClassAlias) {
if (array_key_exists($sExtKeyAttCode, $aExtKeys)) // Skip that check for object keys
{
$sTargetClass = $oModelReflection->GetAttributeProperty($aAliases[$sFromClass], $sExtKeyAttCode, 'targetclass');
if(!$oModelReflection->IsSameFamilyBranch($aAliases[$sToClass], $sTargetClass))
{
if (!$oModelReflection->IsSameFamilyBranch($aAliases[$sToClass], $sTargetClass)) {
throw new OqlNormalizeException("The joined class ($aAliases[$sFromClass]) is not compatible with the external key, which is pointing to $sTargetClass", $sSourceQuery, $oLeftField->GetNameDetails());
}
}
}
else
{
} else {
$sOperator = $oJoinSpec->GetOperator();
switch($sOperator)
{
switch ($sOperator) {
case '=':
$iOperatorCode = TREE_OPERATOR_EQUALS;
break;
$iOperatorCode = TREE_OPERATOR_EQUALS;
break;
case 'BELOW':
$iOperatorCode = TREE_OPERATOR_BELOW;
break;
$iOperatorCode = TREE_OPERATOR_BELOW;
break;
case 'BELOW_STRICT':
$iOperatorCode = TREE_OPERATOR_BELOW_STRICT;
break;
$iOperatorCode = TREE_OPERATOR_BELOW_STRICT;
break;
case 'NOT_BELOW':
$iOperatorCode = TREE_OPERATOR_NOT_BELOW;
break;
$iOperatorCode = TREE_OPERATOR_NOT_BELOW;
break;
case 'NOT_BELOW_STRICT':
$iOperatorCode = TREE_OPERATOR_NOT_BELOW_STRICT;
break;
$iOperatorCode = TREE_OPERATOR_NOT_BELOW_STRICT;
break;
case 'ABOVE':
$iOperatorCode = TREE_OPERATOR_ABOVE;
break;
$iOperatorCode = TREE_OPERATOR_ABOVE;
break;
case 'ABOVE_STRICT':
$iOperatorCode = TREE_OPERATOR_ABOVE_STRICT;
break;
$iOperatorCode = TREE_OPERATOR_ABOVE_STRICT;
break;
case 'NOT_ABOVE':
$iOperatorCode = TREE_OPERATOR_NOT_ABOVE;
break;
$iOperatorCode = TREE_OPERATOR_NOT_ABOVE;
break;
case 'NOT_ABOVE_STRICT':
$iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT;
break;
$iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT;
break;
}
if (array_key_exists($sExtKeyAttCode, $aExtKeys)) // Skip that check for object keys
{
$sTargetClass = $oModelReflection->GetAttributeProperty($aAliases[$sFromClass], $sExtKeyAttCode, 'targetclass');
if(!$oModelReflection->IsSameFamilyBranch($aAliases[$sToClass], $sTargetClass))
{
if (!$oModelReflection->IsSameFamilyBranch($aAliases[$sToClass], $sTargetClass)) {
throw new OqlNormalizeException("The joined class ($aAliases[$sToClass]) is not compatible with the external key, which is pointing to $sTargetClass", $sSourceQuery, $oLeftField->GetNameDetails());
}
}
$aAttList = $oModelReflection->ListAttributes($aAliases[$sFromClass]);
$sAttType = $aAttList[$sExtKeyAttCode];
if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !is_subclass_of($sAttType, \Combodo\iTop\Core\AttributeDefinition\AttributeHierarchicalKey::class) && ($sAttType != \Combodo\iTop\Core\AttributeDefinition\AttributeHierarchicalKey::class))
{
if (($iOperatorCode != TREE_OPERATOR_EQUALS) && !is_a($sAttType, \Combodo\iTop\Core\AttributeDefinition\AttributeHierarchicalKey::class, true)) {
throw new OqlNormalizeException("The specified tree operator $sOperator is not applicable to the key", $sSourceQuery, $oLeftField->GetNameDetails());
}
}
@@ -567,26 +544,23 @@ class OqlObjectQuery extends OqlQuery
// Check the select information
//
foreach ($this->GetSelectedClasses() as $oClassDetails)
{
foreach ($this->GetSelectedClasses() as $oClassDetails) {
$sClassToSelect = $oClassDetails->GetValue();
if (!array_key_exists($sClassToSelect, $aAliases))
{
if (!array_key_exists($sClassToSelect, $aAliases)) {
throw new OqlNormalizeException('Unknown class [alias]', $sSourceQuery, $oClassDetails, array_keys($aAliases));
}
}
// Check the condition tree
//
if ($this->m_oCondition instanceof Expression)
{
if ($this->m_oCondition instanceof Expression) {
$this->m_oCondition->Check($oModelReflection, $aAliases, $sSourceQuery);
}
}
/**
* Make the relevant DBSearch instance (FromOQL)
*/
*/
public function ToDBSearch($sQuery)
{
$sClass = $this->GetClass(new ModelReflectionRuntime());
@@ -594,6 +568,7 @@ class OqlObjectQuery extends OqlQuery
$oSearch = new DBObjectSearch($sClass, $sClassAlias);
$oSearch->InitFromOqlQuery($this, $sQuery);
return $oSearch;
}
}
@@ -606,19 +581,15 @@ class OqlUnionQuery extends OqlQuery
{
parent::__construct();
$this->aQueries[] = $oLeftQuery;
if ($oRightQueryOrUnion instanceof OqlUnionQuery)
{
foreach ($oRightQueryOrUnion->GetQueries() as $oSingleQuery)
{
if ($oRightQueryOrUnion instanceof OqlUnionQuery) {
foreach ($oRightQueryOrUnion->GetQueries() as $oSingleQuery) {
$this->aQueries[] = $oSingleQuery;
}
}
else
{
} else {
$this->aQueries[] = $oRightQueryOrUnion;
}
}
public function GetQueries()
{
return $this->aQueries;
@@ -627,66 +598,54 @@ class OqlUnionQuery extends OqlQuery
/**
* Check the validity of the expression with regard to the data model
* and the query in which it is used
*
* @param ModelReflection $oModelReflection MetaModel to consider
*
* @param ModelReflection $oModelReflection MetaModel to consider
*
* @throws OqlNormalizeException
*/
*/
public function Check(ModelReflection $oModelReflection, $sSourceQuery)
{
$aColumnToClasses = array();
foreach ($this->aQueries as $iQuery => $oQuery)
{
foreach ($this->aQueries as $iQuery => $oQuery) {
$oQuery->Check($oModelReflection, $sSourceQuery);
$aAliasToClass = array($oQuery->GetClassAlias() => $oQuery->GetClass($oModelReflection));
$aJoinSpecs = $oQuery->GetJoins();
if (is_array($aJoinSpecs))
{
foreach ($aJoinSpecs as $oJoinSpec)
{
if (is_array($aJoinSpecs)) {
foreach ($aJoinSpecs as $oJoinSpec) {
$aAliasToClass[$oJoinSpec->GetClassAlias()] = $oJoinSpec->GetClass();
}
}
$aSelectedClasses = $oQuery->GetSelectedClasses();
if ($iQuery != 0)
{
if (count($aSelectedClasses) < count($aColumnToClasses))
{
if ($iQuery != 0) {
if (count($aSelectedClasses) < count($aColumnToClasses)) {
$oLastClass = end($aSelectedClasses);
throw new OqlNormalizeException('Too few selected classes in the subquery', $sSourceQuery, $oLastClass);
}
if (count($aSelectedClasses) > count($aColumnToClasses))
{
if (count($aSelectedClasses) > count($aColumnToClasses)) {
$oLastClass = end($aSelectedClasses);
throw new OqlNormalizeException('Too many selected classes in the subquery', $sSourceQuery, $oLastClass);
}
}
foreach ($aSelectedClasses as $iColumn => $oClassDetails)
{
foreach ($aSelectedClasses as $iColumn => $oClassDetails) {
$sAlias = $oClassDetails->GetValue();
$sClass = $aAliasToClass[$sAlias];
$aColumnToClasses[$iColumn][] = array(
'alias' => $sAlias,
'class' => $sClass,
'alias' => $sAlias,
'class' => $sClass,
'class_name' => $oClassDetails,
);
}
}
foreach ($aColumnToClasses as $iColumn => $aClasses)
{
foreach ($aColumnToClasses as $iColumn => $aClasses) {
$sRootClass = null;
foreach ($aClasses as $iQuery => $aData)
{
if ($iQuery == 0)
{
foreach ($aClasses as $iQuery => $aData) {
if ($iQuery == 0) {
// Establish the reference
$sRootClass = $oModelReflection->GetRootClass($aData['class']);
}
else
{
if ($oModelReflection->GetRootClass($aData['class']) != $sRootClass)
{
} else {
if ($oModelReflection->GetRootClass($aData['class']) != $sRootClass) {
$aSubclasses = $oModelReflection->EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_ALL);
throw new OqlNormalizeException('Incompatible classes: could not find a common ancestor', $sSourceQuery, $aData['class_name'], $aSubclasses);
}
@@ -699,21 +658,21 @@ class OqlUnionQuery extends OqlQuery
* Determine the class
*
* @param ModelReflection $oModelReflection MetaModel to consider
*
* @return string
* @throws Exception
*/
public function GetClass(ModelReflection $oModelReflection)
{
$aFirstColClasses = array();
foreach ($this->aQueries as $iQuery => $oQuery)
{
foreach ($this->aQueries as $iQuery => $oQuery) {
$aFirstColClasses[] = $oQuery->GetClass($oModelReflection);
}
$sClass = self::GetLowestCommonAncestor($oModelReflection, $aFirstColClasses);
if (is_null($sClass))
{
if (is_null($sClass)) {
throw new Exception('Could not determine the class of the union query. This issue should have been detected earlier by calling OqlQuery::Check()');
}
return $sClass;
}
@@ -726,6 +685,7 @@ class OqlUnionQuery extends OqlQuery
public function GetClassAlias()
{
$sAlias = $this->aQueries[0]->GetClassAlias();
return $sAlias;
}
@@ -735,29 +695,25 @@ class OqlUnionQuery extends OqlQuery
*
* @param ModelReflection $oModelReflection MetaModel to consider
* @param array $aClasses Flat list of classes
*
* @return string the lowest common ancestor amongst classes, null if none has been found
* @throws Exception
*/
public static function GetLowestCommonAncestor(ModelReflection $oModelReflection, $aClasses)
{
$sAncestor = null;
foreach($aClasses as $sClass)
{
if (is_null($sAncestor))
{
foreach ($aClasses as $sClass) {
if (is_null($sAncestor)) {
// first loop
$sAncestor = $sClass;
}
elseif ($oModelReflection->GetRootClass($sClass) != $oModelReflection->GetRootClass($sAncestor))
{
} elseif ($oModelReflection->GetRootClass($sClass) != $oModelReflection->GetRootClass($sAncestor)) {
$sAncestor = null;
break;
}
else
{
} else {
$sAncestor = self::LowestCommonAncestor($oModelReflection, $sAncestor, $sClass);
}
}
return $sAncestor;
}
@@ -766,37 +722,32 @@ class OqlUnionQuery extends OqlQuery
*/
protected static function LowestCommonAncestor(ModelReflection $oModelReflection, $sClassA, $sClassB)
{
if ($sClassA == $sClassB)
{
if ($sClassA == $sClassB) {
$sRet = $sClassA;
}
elseif (in_array($sClassA, $oModelReflection->EnumChildClasses($sClassB)))
{
} elseif (in_array($sClassA, $oModelReflection->EnumChildClasses($sClassB))) {
$sRet = $sClassB;
}
elseif (in_array($sClassB, $oModelReflection->EnumChildClasses($sClassA)))
{
} elseif (in_array($sClassB, $oModelReflection->EnumChildClasses($sClassA))) {
$sRet = $sClassA;
}
else
{
} else {
// Recurse
$sRet = self::LowestCommonAncestor($oModelReflection, $sClassA, $oModelReflection->GetParentClass($sClassB));
}
return $sRet;
}
/**
* Make the relevant DBSearch instance (FromOQL)
*/
*/
public function ToDBSearch($sQuery)
{
$aSearches = array();
foreach ($this->aQueries as $oQuery)
{
foreach ($this->aQueries as $oQuery) {
$aSearches[] = $oQuery->ToDBSearch($sQuery);
}
$oSearch = new DBUnionSearch($aSearches);
return $oSearch;
}
}

View File

@@ -415,12 +415,7 @@ abstract class User extends cmdbAbstractObject
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:CurrentProfilesHaveInsufficientRights');
}
$oAddon->ResetCache();
if (is_null($aCurrentProfiles)) {
Session::IsSet('profile_list');
} else {
Session::Set('profile_list', $aCurrentProfiles);
}
Session::Set('profile_list', $aCurrentProfiles);
}
// Prevent an administrator to remove their own admin profile
if (UserRights::IsAdministrator($this)) {

View File

@@ -22,6 +22,4 @@
@import "medallion-with-blocklist";
@import "field-badge-within-datatable";
@import "jquery-blockui-within-dialog";
@import "jquery-blockui-within-datatable";
@import "badge-with-badge";
@import "extension-details-with-extension-details";
@import "jquery-blockui-within-datatable";

View File

@@ -1,10 +0,0 @@
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
$ibo-badge--spacing-left--with-same-block: $ibo-spacing-200 !default;
.ibo-badge + .ibo-badge {
margin-left: $ibo-badge--spacing-left--with-same-block;
}

View File

@@ -1,11 +0,0 @@
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
$ibo-extension-details--margin-top: $ibo-spacing-300 !default;
.ibo-extension-details + .ibo-extension-details,
.ibo-extension-details--information--description .ibo-extension-details {
margin-top: $ibo-extension-details--margin-top;
}

View File

@@ -4,7 +4,7 @@
*/
$ibo-field--spacing-top--with-same-block: $ibo-spacing-500 !default;
.ibo-field + .ibo-field {
.ibo-field + .ibo-field:not(:empty) {
margin-top: $ibo-field--spacing-top--with-same-block;
}

View File

@@ -33,5 +33,4 @@
@import "field-badge";
@import "file-select";
@import "medallion-icon";
@import "toast";
@import "badge";
@import "toast";

View File

@@ -1,41 +0,0 @@
$ibo-badge--padding-x : $ibo-spacing-200 !default;
$ibo-badge--padding-y : $ibo-spacing-100 !default;
$ibo-badge--border-radius : $ibo-border-radius-400 !default;
$ibo-badge-colors: (
'primary': ($ibo-color-primary-100, $ibo-color-primary-900),
'secondary': ($ibo-color-secondary-100, $ibo-color-secondary-900),
'neutral': ($ibo-color-secondary-100, $ibo-color-secondary-900),
'information': ($ibo-color-information-100, $ibo-color-information-900),
'success': ($ibo-color-success-100, $ibo-color-success-900),
'failure': ($ibo-color-danger-100, $ibo-color-danger-900),
'warning': ($ibo-color-warning-100,$ibo-color-warning-900),
'danger': ($ibo-color-danger-100,$ibo-color-danger-900),
'grey' : ($ibo-color-grey-100, $ibo-color-grey-900),
'blue-grey': ($ibo-color-blue-grey-100, $ibo-color-blue-grey-900),
'blue': ($ibo-color-blue-100, $ibo-color-blue-900),
'cyan': ($ibo-color-cyan-100, $ibo-color-cyan-900),
'green': ($ibo-color-green-100, $ibo-color-green-900),
'orange' : ($ibo-color-orange-100, $ibo-color-orange-900),
'red': ($ibo-color-red-100, $ibo-color-red-900),
'pink': ($ibo-color-pink-100, $ibo-color-pink-900),
) !default;
.ibo-badge {
display: inline-block;
white-space: nowrap;
padding : $ibo-badge--padding-y $ibo-badge--padding-x;
border-radius : $ibo-badge--border-radius;
@extend %ibo-font-ral-med-50;
@each $sColor, $aColorValues in $ibo-badge-colors {
$bg-color: nth($aColorValues, 1);
$text-color: nth($aColorValues, 2);
&.ibo-is-#{$sColor} {
background-color: $bg-color;
color: $text-color;
}
}
}

View File

@@ -6,4 +6,95 @@
.ibo-prop-header {
@extend %ibo-font-size-150;
padding-bottom: 14px;
}
}
.help-text{
padding: 1px 5px;
background-color: #d7e3f8;
border: 1px solid #c6e7f5;
border-radius: 5px;
margin: 5px 0;
font-size: 0.9em;
}
.form-error ul{
padding: 1px 5px;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 5px;
margin: 5px 0;
font-size: 0.9em;
}
.subform{
background-color: #efefef;
border-radius: 5px;
padding: 10px;
}
.form-buttons{
margin: 20px 0;
}
.form select{
padding: 0;
overflow-y: auto;
}
.form select option{
height: 30px;
display: flex;
align-items: center;
}
.turbo-refreshing{
opacity: .5;
}
.ibo-field legend{
margin-top: 24px;
}
collection-entry-element {
margin-top: 8px;
display: block;
padding: 10px 10px;
background-color: #f5f5f5;
border-radius: 5px;
}
.ts-control{
height: auto;
min-height: 30px;
}
.ibo-form-actions > .ibo-button > span{
margin-right: 5px;
}
.ibo-form textarea{
resize: vertical;
}
.ibo-form-compact{
.ibo-field{
display: flex;
flex-direction: column;
gap: 4px;
label{
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: unset;
padding-right: 0;
}
}
}

View File

@@ -19,7 +19,10 @@ $ibo-dashlet-blocker--height: 100% !default;
.ibo-dashlet {
position: relative;
width: calc(#{$ibo-dashlet--width} - #{$ibo-dashlet--elements-spacing-x});
margin: calc(#{$ibo-dashlet--elements-spacing-y} / 2) calc(#{$ibo-dashlet--elements-spacing-x} / 2);
//margin: calc(#{$ibo-dashlet--elements-spacing-y} / 2) calc(#{$ibo-dashlet--elements-spacing-x} / 2);
height: 100% !important;
width: 100% !important;
&.dashlet-selected {
position: relative;
@@ -38,4 +41,21 @@ $ibo-dashlet-blocker--height: 100% !default;
width: $ibo-dashlet-blocker--width;
height: $ibo-dashlet-blocker--height;
cursor: not-allowed;
}
.ibo-dashlet--actions {
position: absolute;
z-index: 1000;
top: 0px;
right: 0px;
display: none;
padding: 4px;
border-radius: 4px;
background-color: $ibo-color-white-100;
@extend %ibo-elevation-100;
}
ibo-dashlet[data-edit-mode="edit"] {
z-index: 3;
}

View File

@@ -54,3 +54,31 @@ $ibo-input-select-icon--menu--icon--margin-right: 10px !default;
}
}
}
.ts-control > .ibo-input-select-icon--menu--item{
display: flex;
column-gap: 5px;
align-items: center;
padding: 5px 0;
> img{
width: 20px;
}
&:hover{
background: transparent;
}
}
.ts-dropdown > .ts-dropdown-content > .ibo-input-select-icon--menu--item{
display: flex;
column-gap: 5px;
> img{
width: 32px;
}
}

View File

@@ -8,7 +8,6 @@ $ibo-toggler--wrapper--height: 20px !default;
$ibo-toggler--slider--border-radius: $ibo-border-radius-900 !default;
$ibo-toggler--slider--background-color: $ibo-color-secondary-600 !default;
$ibo-toggler--slider--disabled--background-color: $ibo-color-secondary-200 !default;
$ibo-toggler--slider--before--left: 3px !default;
$ibo-toggler--slider--before--bottom: 3px !default;
@@ -18,7 +17,6 @@ $ibo-toggler--slider--before--border-radius: $ibo-border-radius-full !default;
$ibo-toggler--slider--before--background-color: $ibo-color-grey-100 !default;
$ibo-toggler--slider--checked--background-color: $ibo-color-primary-600 !default;
$ibo-toggler--slider--checked-disabled--background-color: $ibo-color-primary-200 !default;
$ibo-toggler--slider--focus--box-shadow: 0 0 1px $ibo-color-primary-600 !default;
$ibo-toggler--label--margin-left: 4px !default;
@@ -63,13 +61,6 @@ $ibo-toggler--label--margin-left: 4px !default;
background-color: $ibo-toggler--slider--checked--background-color;
}
.ibo-toggler--wrapper input:disabled + .ibo-toggler--slider {
background-color: $ibo-toggler--slider--disabled--background-color;
}
.ibo-toggler--wrapper input:checked:disabled + .ibo-toggler--slider {
background-color: $ibo-toggler--slider--checked-disabled--background-color;
}
input:focus + .ibo-toggler--slider {
box-shadow: $ibo-toggler--slider--focus--box-shadow;
}

View File

@@ -15,4 +15,4 @@
@import "wizard-container/wizard-container";
@import "object/all";
@import "activity-panel/all";
@import "extension/all";
@import "dashlet-panel/all";

View File

@@ -175,3 +175,73 @@ input:checked + .ibo-dashboard--slider:before {
input:checked + .ibo-dashboard--slider:after {
content: $ibo-dashboard--slider--before--content;
}
// TODO 3.3 Cleanup variables
// TODO 3.3 Move to vendor what's from gridstack
.grid-stack {
display: block;
}
.ibo-dashboard[data-edit-mode="edit"] .grid-stack{
background-size: calc(100% / 12) var(--gs-cell-height);
background-color: $ibo-color-white-100;
background-image: linear-gradient(to right, $ibo-color-white-200 8px, transparent 8px), linear-gradient(to bottom, $ibo-color-white-200 8px, transparent 8px);
--gs-item-margin-top: 8px;
--gs-item-margin-bottom: 0;
--gs-item-margin-right: 0;
--gs-item-margin-left: 8px;
}
ibo-dashboard[data-edit-mode="view"] {
.ibo-dashboard--form {
display: none;
}
}
.ibo-dashboard--form {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 55px;
background-color: $ibo-color-blue-200;
margin: -16px -36px 24px -36px;
padding: 0 36px;
}
.ibo-dashboard--form--inputs {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
@extend %common-font-ral-med-250;
}
.ibo-dashboard[data-edit-mode="edit"] ibo-dashlet:not([data-edit-mode="edit"]):hover .ibo-dashlet--actions {
display: block;
}
.ibo-dashboard[data-edit-mode="error"] .grid-stack{
background-image: url($approot-relative + '/images/alpha-fatal-error.gif');
}
// Our edit mode dashboard already has its own header, so we hide the standard one
#ibo-page-header:has(+ ibo-dashboard[data-edit-mode="edit"]) {
display: none;
}
.ibo-dashboard[data-edit-mode="edit"] .ibo-dashboard--grid:has(ibo-dashboard-grid-slot > ibo-dashlet[data-edit-mode="edit"]) .ibo-dashboard--grid--backdrop {
position: absolute;
height: calc(100% + 24px);
// 36px is $ibo-page-container--elements-padding-x, handle variable resolution
width: calc(100% + 36px + 36px);
margin: -24px -#{36px} 0 -#{36px};
background-color: $ibo-color-grey-400;
z-index: 2;
opacity: 60%;
}

View File

@@ -0,0 +1,2 @@
@import "dashlet-entry";
@import "dashlet-panel";

View File

@@ -0,0 +1,51 @@
// TODO 3.3 Cleanup variables
.ibo-dashlet-entry {
display: flex;
flex-direction: row;
gap: 10px;
border: 1px solid $ibo-color-grey-300;
border-radius: 5px;
padding: 12px;
background-color: $ibo-color-grey-100;
cursor: pointer;
text-align: left;
&:hover {
background-color: $ibo-color-grey-200;
border-color: $ibo-color-grey-400;
}
&:active {
background-color: $ibo-color-grey-50;
border-color: $ibo-color-grey-500;
}
}
.ibo-dashlet-entry--icon {
flex-shrink: 0;
height: 36px;
width: 36px;
}
.ibo-dashlet-entry--content {
display: flex;
flex-direction: column;
justify-content: center;
overflow-x: hidden;
gap: 4px;
}
.ibo-dashlet-entry--title {
font-size: 14px;
font-weight: 600;
color: $ibo-color-grey-900;
}
.ibo-dashlet-entry--description {
font-size: 12px;
color: $ibo-color-grey-700;
}

View File

@@ -0,0 +1,66 @@
// TODO 3.3 Cleanup variables
.ibo-dashlet-panel {
height: 100%;
display: flex;
flex-direction: column;
width: 326px;
background-color: $ibo-color-white-100;
}
.ibo-dashlet-panel--title {
display: flex;
flex-direction: column;
justify-content: center;
height: 55px;
background-color: $ibo-color-white-200;
padding: 0 16px;
flex-grow: 0;
@extend %common-font-ral-med-300;
}
.ibo-dashlet-panel--entries, .ibo-dashlet-panel--form-container {
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: auto;
padding: 16px;
gap: 12px;
&.ibo-is-hidden {
display: none;
}
}
.ibo-center-container:has(ibo-dashboard[data-edit-mode="view"]) .ibo-dashlet-panel{
display: none;
}
.ibo-dashlet-panel--form-container turbo-frame {
height: 100%;
}
.ibo-dashlet-panel--form-container .ibo-form {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
> .form {
overflow: auto;
margin-bottom: 16px;
}
}
.ibo-dashlet-panel--form-container--buttons {
display: flex;
flex-direction: row !important;
justify-content: end;
align-items: center;
margin: 0 -16px -16px -16px;
padding: 0 16px;
min-height: 60px;
background-color: $ibo-color-grey-50;
border-top: solid 1px $ibo-color-grey-400;
}

View File

@@ -1 +0,0 @@
@import "extension-details";

View File

@@ -1,65 +0,0 @@
$ibo-extension-details--information--metadata--padding: $ibo-spacing-200 !default;
$ibo-extension-details--information--metadata--delimiter: "-" !default;
$ibo-extension-details--information--metadata--color: $ibo-color-grey-700 !default;
$ibo-extension-details--actions--button--padding-y: 3px !default;
$ibo-extension-details--actions--button--padding-x: $ibo-button--padding-x !default;
.ibo-extension-details {
display: inline-flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
}
.ibo-extension-details--information {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.ibo-extension-details--actions {
display: flex;
}
.ibo-extension-details--information--label {
@extend %ibo-font-ral-med-150;
}
.ibo-extension-details--information--metadata {
@extend %ibo-font-ral-med-100;
color: $ibo-extension-details--information--metadata--color;
}
.ibo-extension-details--information--description {
@extend %ibo-font-ral-med-100;
}
.ibo-extension-details--information--metadata span + span:before {
content: $ibo-extension-details--information--metadata--delimiter;
padding-left: $ibo-extension-details--information--metadata--padding;
padding-right: $ibo-extension-details--information--metadata--padding;
}
//ibo-extension-details can have other ibo-extension-details inside its ibo-extension-details--information--description in the setup. We need to only affect direct children
.ibo-extension-details:has(>.ibo-extension-details--actions input:is([type="checkbox"], [type="radio"]):checked){
&>.ibo-extension-details--information>.ibo-extension-details--information--label .ibo-badge.unchecked {
display: none;
}
}
//Merging the two lines below with :is([type="checkbox"], [type="radio"]) will generate a warning in scss compiler
.ibo-extension-details:has(>.ibo-extension-details--actions input[type="checkbox"]:not(:checked)),
.ibo-extension-details:has(>.ibo-extension-details--actions input[type="radio"]:not(:checked)) {
&>.ibo-extension-details--information>.ibo-extension-details--information--label .ibo-badge.checked {
display: none;
}
}
.ibo-extension-details--actions > button {
padding: $ibo-extension-details--actions--button--padding-y $ibo-extension-details--actions--button--padding-x;
}
.ibo-extension-details--actions:has(.toggler-install:not(:disabled)) .ibo-popover-menu--section a[data-resource-id="force_uninstall"] {
display: none;
}

View File

@@ -0,0 +1,3 @@
@import "../../../node_modules/tom-select/dist/scss/tom-select.scss";
$select-color-item-active-border: $ibo-input--focus--border-color;

File diff suppressed because one or more lines are too long

View File

@@ -316,34 +316,29 @@ fieldset {
background-color: #F7FAFC;
padding: 10px;
.wiz-choice{
&:not(:checked) ~ label .checked{
&: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 .unchecked{
&:checked ~ label .setup-extension-tag.unchecked{
display:none;
}
}
}
.ibo-extension-details:has(>.ibo-extension-details--actions>input:checked) {
.ibo-extension-details:has(#itop-ticket-mgmt-simple-ticket-enhanced-portal:not(:checked), #itop-ticket-mgmt-itil-enhanced-portal:not(:checked)) {
.ibo-extension-details--information--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;
}
}
}
.ibo-extension-details--information--metadata{
color: $ibo-color-grey-800;
}
.choice-disabled {
color: $ibo-color-grey-700;
}
body {
font-size: 1.17rem;
@@ -527,12 +522,10 @@ body {
}
}
.ibo-setup-summary-title, .ibo-setup-summary-title:visited, .ibo-setup-summary-title:hover {
font-size: $ibo-font-size-150;
color: inherit;
.ibo-setup-summary-title {
font-size: $ibo-font-size-150;
}
#ibo-setup-licenses--components-list {
background-color: $ibo-color-white-200;
padding: 12px;
@@ -612,7 +605,6 @@ body {
color:#a00000;
}
.setup-extension-tag {
display: inline-flex;
background-color: grey;
border-radius: 8px;
padding-left: 3px;
@@ -638,21 +630,6 @@ body {
}
}
.ibo-extension-details {
align-items: flex-start;
}
.ibo-extension-details--actions input{
margin:0.2em 0.5em;
width: 12px;
}
:not(.ibo-badge) ~ .ibo-badge{
margin-left:0.5em;
}
.ibo-extension-details--information--label i{
font-size : 0.9em;
margin-left:0.3em;
}
.setup--wizard-choice--label + .setup--wizard-choice--more-info {
margin-left: 0.5rem;
}
@@ -704,6 +681,9 @@ body {
overflow: auto;
text-align: center;
}
#installation_progress {
display: none;
}
#fresh_content{
border: 0;
min-height: 300px;

View File

@@ -1,9 +0,0 @@
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/*
* CSS of the template page
*/

View File

@@ -1,9 +0,0 @@
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/*
* Javascript file loaded in template page
*/

View File

@@ -1,17 +0,0 @@
{
"config": {
"classmap-authoritative": true
},
"autoload": {
"psr-4": {
"Combodo\\iTop\\DataFeatureRemoval\\": "src",
"": "src/NoNamespace"
}
},
"name": "combodo/combodo-data-feature-removal",
"type": "itop-extension",
"description": "iTop Data Feature Removal",
"require": {
"composer-runtime-api": "^2.0"
}
}

View File

@@ -1,20 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b862a55cbf5448fb99f0905a4db6529b",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"composer-runtime-api": "^2.0"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View File

@@ -1,127 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.3">
<classes>
<class id="DataFeatureRemovalBackgroundOperation" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>datafeatureremovalbackgroundoperation</db_table>
<style>
<icon>
<fileref ref="icons8-electricity_643cf7fd7a024968679dc0c35a710a03"/>
</icon>
<main_color>#0000ff</main_color>
</style>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
<uniqueness_rules>
<rule id="list_of_classes">
<attributes>
<attribute id="classes"/>
</attributes>
<filter><![CDATA[]]></filter>
<disabled>false</disabled>
<is_blocking>true</is_blocking>
</rule>
</uniqueness_rules>
</properties>
<fields>
<field id="creation_date" xsi:type="AttributeDateTime">
<sql>creation_date</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="features_code" xsi:type="AttributeText">
<sql>features_code</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
<width/>
<height/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="classes" xsi:type="AttributeText">
<sql>classes</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
<width/>
<height/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items>
<item id="creation_date">
<rank>10</rank>
</item>
<item id="features_code">
<rank>20</rank>
</item>
<item id="classes">
<rank>30</rank>
</item>
</items>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="creation_date">
<rank>10</rank>
</item>
<item id="features_code">
<rank>20</rank>
</item>
<item id="classes">
<rank>30</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<dictionaries>
<dictionary id="EN US">
<entries>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation" _delta="define"><![CDATA[DataFeatureRemovalBackgroundOperation]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:creation_date" _delta="define"><![CDATA[Creation date]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:creation_date+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:features_code" _delta="define"><![CDATA[Features code]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:features_code+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:classes" _delta="define"><![CDATA[Classes]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:classes+" _delta="define"><![CDATA[]]></entry>
</entries>
</dictionary>
</dictionaries>
<files>
<file id="icons8-electricity_643cf7fd7a024968679dc0c35a710a03" xsi:type="File" _delta="define_if_not_exists">
<name>images/icons/icons8-electricity.svg</name>
<mime_type>image/svg+xml</mime_type>
<data>PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSIyNDBweCIgaGVpZ2h0PSIyNDBweCI+PHBhdGggZmlsbD0iIzM3YzZkMCIgZD0iTTI2LjQ1MTIsMzdsNy43NzY0Ni0xNS41NTI5MWExLDEsMCwwLDAtLjg5NDM1LTEuNDQ3MjJMMjIuOTUxMiwxOS45OTkyNWw2LjMzNi0xMy41NzYzNUExLDEsMCwwLDAsMjguMzgxLDVIMjIuNTk2MzlhMSwxLDAsMCwwLS45MTExMS41ODc4M2wtOC41OTUyNCwxOUExLDEsMCwwLDAsMTQuMDAxMTUsMjZoMTAuNDVsLTUuNSwxMVoiLz48cGF0aCBmaWxsPSIjMzdjNmQwIiBkPSJNMTYuMjIyODYsMzdIMjkuODY0NzFhLjU1NzM1LjU1NzM1LDAsMCwxLC4zNTgxNSwxTDE5Ljk2LDQ0LjM2N2ExLjAzMzY0LDEuMDMzNjQsMCwwLDEtMS40OTEyMS0uNDI1ODZMMTUuNzIyODYsMzcuNzVBLjUwNDUuNTA0NSwwLDAsMSwxNi4yMjI4NiwzN1oiLz48L3N2Zz4=</data>
</file>
</files>
<menus>
<menu id="DataFeatureRemovalMenu" xsi:type="WebPageMenuNode" _delta="define">
<rank>30</rank>
<parent>SystemTools</parent>
<url>$pages/exec.php?exec_module=combodo-data-feature-removal&amp;exec_page=index.php&amp;c[menu]=DataFeatureRemovalMenu</url>
<enable_admin_only>1</enable_admin_only>
</menu>
</menus>
</itop_design>

View File

@@ -1,59 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Localized data
*/
Dict::Add('EN US', 'English', 'English', [
'Menu:DataFeatureRemovalMenu' => 'Features Removal',
'combodo-data-feature-removal/Operation:Main/Title' => 'Features Removal',
'DataFeatureRemoval:Main:Title' => 'Features Removal',
'DataFeatureRemoval:Main:SubTitle' => 'Prepare features you want to enable/disable in a future setup',
'DataFeatureRemoval:Failure:Title' => 'Feature dry removal errors',
'DataFeatureRemoval:Helper:Title' => 'Enable or disable features that are installed in your iTop.',
'DataFeatureRemoval:Helper:Desc1' => 'It will prepare the setup step that proceeds to feature enabling or disabling.',
'DataFeatureRemoval:Helper:Desc2' => 'Analyze if there are any data or dependency preventing you from enabling/disabling a feature.',
'DataFeatureRemoval:Features:Title' => 'Features',
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing',
'DataFeatureRemoval:Analysis:Ok' => "No data to cleanup",
'DataFeatureRemoval:DeletionPlan:Title' => 'Deletion plan',
'DataFeatureRemoval:DeletionPlan:SubTitle' => 'Database tables to clean before continuing',
'DataFeatureRemoval:DoDeletion:Title' => 'Do deletion',
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Remove all the entries from the database',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name',
'DataFeatureRemoval:Table:Analysis:Module' => 'Module name',
'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Occurrence',
'UI:Button:Analyze' => 'Analyze',
'UI:Button:ModifyChoices' => 'Modify Choices',
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',
'UI:Button:PlanDeletion' => 'Prepare deletion plan',
'UI:Button:DoDeletion' => 'Delete data',
'UI:Button:DoAsyncDeletion' => 'Do asynchronous deletion',
'UI:Button:BackToMain' => 'Back to Feature Removal',
'UI:Button:Setup' => 'Back to setup',
'UI:Action:ForceUninstall' => 'Force uninstall',
'UI:Action:MoreInfo' => 'More information',
'DataFeatureRemoval:Table:Empty' => 'No data to remove',
'DataFeatureRemoval:Column:Class' => 'Class',
'DataFeatureRemoval:Column:DeleteCount' => 'Entries to delete',
'DataFeatureRemoval:Column:UpdateCount' => 'Entries to update',
'DataFeatureRemoval:Column:Issue' => 'Issue',
'DataFeatureRemoval:Column:DeletedCount' => 'Deleted entries',
'DataFeatureRemoval:Column:UpdatedCount' => 'Updated entries',
]);

View File

@@ -1,59 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Localized data
*/
Dict::Add('FR FR', 'French', 'Français', [
'Menu:DataFeatureRemovalMenu' => 'Features Removal',
'combodo-data-feature-removal/Operation:Main/Title' => 'Features Removal',
'DataFeatureRemoval:Main:Title' => 'Features Removal',
'DataFeatureRemoval:Main:SubTitle' => 'Prepare features you want to enable/disable in a future setup',
'DataFeatureRemoval:Failure:Title' => 'Feature dry removal errors',
'DataFeatureRemoval:Helper:Title' => 'Enable or disable features that are installed in your iTop.',
'DataFeatureRemoval:Helper:Desc1' => 'It will prepare the setup step that proceeds to feature enabling or disabling.',
'DataFeatureRemoval:Helper:Desc2' => 'Analyze if there are any data or dependency preventing you from enabling/disabling a feature.',
'DataFeatureRemoval:Features:Title' => 'Features',
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing',
'DataFeatureRemoval:Analysis:Ok' => "No data to cleanup",
'DataFeatureRemoval:DeletionPlan:Title' => 'Deletion plan',
'DataFeatureRemoval:DeletionPlan:SubTitle' => 'Database tables to clean before continuing',
'DataFeatureRemoval:DoDeletion:Title' => 'Do deletion',
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Remove all the entries from the database',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name',
'DataFeatureRemoval:Table:Analysis:Module' => 'Module name',
'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Occurrence',
'UI:Button:Analyze' => 'Analyze',
'UI:Button:ModifyChoices' => 'Modify Choices',
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',
'UI:Button:PlanDeletion' => 'Prepare deletion plan',
'UI:Button:DoAsyncDeletion' => 'Do asynchronous deletion',
'UI:Button:DoDeletion' => 'Delete data',
'UI:Button:BackToMain' => 'Back to Feature Removal',
'UI:Button:Setup' => 'Back to setup',
'UI:Action:ForceUninstall' => 'Force uninstall',
'UI:Action:MoreInfo' => 'More information',
'DataFeatureRemoval:Table:Empty' => 'No data to remove',
'DataFeatureRemoval:Column:Class' => 'Class',
'DataFeatureRemoval:Column:DeleteCount' => 'Entries to delete',
'DataFeatureRemoval:Column:UpdateCount' => 'Entries to update',
'DataFeatureRemoval:Column:Issue' => 'Issue',
'DataFeatureRemoval:Column:DeletedCount' => 'Deleted entries',
'DataFeatureRemoval:Column:UpdatedCount' => 'Updated entries',
]);

View File

@@ -1,20 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval;
use Combodo\iTop\DataFeatureRemoval\Controller\DataFeatureRemovalController;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
require_once(APPROOT.'application/startup.inc.php');
DataFeatureRemovalLog::Enable();
$oController = new DataFeatureRemovalController(MODULESROOT.DataFeatureRemovalHelper::MODULE_NAME.'/templates', DataFeatureRemovalHelper::MODULE_NAME);
$oController->SetDefaultOperation('Main');
$oController->HandleOperation();

View File

@@ -1,16 +0,0 @@
<?php
// PHP Data Model definition file
// WARNING - WARNING - WARNING
// DO NOT EDIT THIS FILE (unless you know what you are doing)
//
// If you provide a datamodel.xxxx.xml file with your module,
// this file WILL BE overwritten by the compilation of the
// module (during the setup) if the datamodel.xxxx.xml file
// contains the definition of new classes or menus.
//
// The recommended way to define new classes (for iTop 2.0 and later) is via the XML definition.
// This file remains in the module's template only for the cases where there is:
// - either no new class or menu defined in the XML file
// - or no XML file at all supplied by the module

View File

@@ -1,55 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
//
// iTop module definition file
//
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'combodo-data-feature-removal/3.3.0',
[
// Identification
//
'label' => 'iTop Data Feature Removal',
'category' => 'business',
// Setup
//
'dependencies' => [
],
'mandatory' => true,
'visible' => false,
// Components
//
'datamodel' => [
'vendor/autoload.php',
'model.combodo-data-feature-removal.php', // Contains the PHP code generated by the "compilation" of datamodel.combodo-data-feature-removal.xml
'src/Hook/DataFeatureRemovalBackgroundTask.php',
],
'webservice' => [],
'data.struct' => [
// add your 'structure' definition XML files here,
],
'data.sample' => [
// add your sample data XML files here,
],
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
'doc.more_information' => '', // hyperlink to more information, if any
// Default settings
//
'settings' => [
// Module specific settings go here, if any
],
]
);

View File

@@ -1,276 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Controller;
require_once APPROOT.'setup/feature_removal/SetupAudit.php';
require_once APPROOT.'setup/feature_removal/DryRemovalRuntimeEnvironment.php';
use Combodo\iTop\Application\TwigBase\Controller\Controller;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Service\BackgroundOperationService;
use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService;
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
use Dict;
use Exception;
use IssueLog;
use MetaModel;
use utils;
class DataFeatureRemovalController extends Controller
{
private array $aSelectedExtensionsForCheck = [];
private array $aCountClassesToCleanup = [];
private array $aAnalysisDataTable = [];
private int $iCount = 0;
public function OperationMain($sErrorMessage = null): void
{
$aParams = [];
$this->ReadRemovedExtensions();
$this->AddAnalyzeParams();
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aExtensions'] = $this->GetExtensionsTable();
$aParams['aExtensionsCode'] = $this->aSelectedExtensionsForCheck;
$aParams['aAnalysisDataTable'] = $this->aAnalysisDataTable;
$aParams['aClasses'] = array_keys($this->aCountClassesToCleanup);
$aParams['bHasData'] = $this->iCount > 0;
$aParams['sSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup';
$aParams['iCount'] = $this->iCount;
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
$aParams['bAnalysisOk'] = (count($this->aCountClassesToCleanup) > 0) && ($this->iCount === 0);
$this->AddLinkedStylesheet(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/css/DataFeatureRemoval.css');
$this->AddLinkedScript(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/js/DataFeatureRemoval.js');
$this->m_sOperation = "Main";
$this->DisplayPage($aParams);
}
public function AddAnalyzeParams(): void
{
$aData = [];
$aColumns = [];
$this->iCount = 0;
foreach ($this->aCountClassesToCleanup as $sClass => $iCount) {
$sModuleName = MetaModel::GetModuleName($sClass);
$aExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetIncludingExtensions($sModuleName);
$sExtensions = implode(' ', $aExtensions);
$aColumns = ['ClassName','FeatureName','Module','Occurrence'];
$aData[] = [$sClass,$sExtensions,$sModuleName,$iCount];
$this->iCount += $iCount;
}
$this->aAnalysisDataTable = $this->GetTableData('Analysis', $aColumns, $aData);
}
public function OperationAnalyze(): void
{
$this->ReadRemovedExtensions();
$this->m_sOperation = 'Main';
try {
if (count($this->aSelectedExtensionsForCheck) > 0) {
$this->Analyze();
}
$this->OperationMain();
} catch (Exception $e) {
IssueLog::Error(__METHOD__, null, ['stack' => $e->getTraceAsString(), 'exception' => $e->getMessage()]);
$this->OperationMain($e->getMessage());
}
}
private function Analyze(): void
{
$sSourceEnv = MetaModel::GetEnvironment();
$oDryRemovalRuntimeEnvironment = new DryRemovalRuntimeEnvironment();
$oDryRemovalRuntimeEnvironment->Prepare($sSourceEnv, $this->aSelectedExtensionsForCheck);
$oDryRemovalRuntimeEnvironment->CompileFrom($sSourceEnv);
$oSetupAudit = new SetupAudit($sSourceEnv, DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV);
$aGetRemovedClasses = $oSetupAudit->GetIssues();
IssueLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]);
$this->aCountClassesToCleanup = $aGetRemovedClasses;
}
public function OperationDeletionPlan(): void
{
$aParams = [];
$this->ValidateTransactionId();
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$aDeletionPlanSummaryEntities = DeletionPlanService::GetInstance()->GetDeletionPlanSummary($aClasses);
$aColumns = ['Class', 'DeleteCount' , 'UpdateCount', 'Issue'];
$aRows = [];
foreach ($aDeletionPlanSummaryEntities as $oDeletionPlanSummaryEntity) {
$aRows[] = [
$oDeletionPlanSummaryEntity->sClass,
$oDeletionPlanSummaryEntity->iDeleteCount,
$oDeletionPlanSummaryEntity->iUpdateCount,
$oDeletionPlanSummaryEntity->sIssue ?? '',
];
}
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aDeletionPlanSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows);
$aParams['aClasses'] = $aClasses;
$aParams['aExtensionsCode'] = utils::ReadPostedParam('aExtensionsCode', []);
$this->DisplayPage($aParams);
}
public function OperationDoDeletion(): void
{
$aParams = [];
$this->ValidateTransactionId();
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$sDeletionButtonValue = utils::ReadPostedParam('btn_deletion', null);
$bAsynchronous = ('async_deletion' === $sDeletionButtonValue);
if ($bAsynchronous) {
BackgroundOperationService::GetInstance()->CreateOperation(utils::ReadPostedParam('aExtensionsCode', []), $aClasses);
$this->OperationMain();
return;
}
$aDeletionExecutionSummary = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
$aColumns = ['Class', 'DeletedCount' , 'UpdatedCount'];
$aRows = [];
foreach ($aDeletionExecutionSummary as $oDeletionExecutionSummaryEntity) {
$aRows[] = [
$oDeletionExecutionSummaryEntity->sClass,
$oDeletionExecutionSummaryEntity->iDeleteCount,
$oDeletionExecutionSummaryEntity->iUpdateCount,
];
}
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aDeletionExecutionSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows);
$this->DisplayPage($aParams);
}
/**
* Get installed extensions from disk
*
* @return array structure for twig datatable
*/
private function GetExtensionsTable(): array
{
$aExtensions = [];
$aColumns = ['', 'Version', 'Name', 'Code'];
foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) {
/** @var \iTopExtension $oExtension */
$bCleanupOngoing = BackgroundOperationService::GetInstance()->IsExtensionBeingCleaned($sCode);
$sChecked = '';
$sDisabledHtml = '';
$sLabel = $oExtension->sLabel;
if ($bCleanupOngoing) {
$sDisabledHtml = 'disabled=""';
$sLabel .= <<<HTML
&nbsp; <span class="ibo-spinner ibo-is-inline ibo-spinner ibo-block" data-role="ibo-spinner"><i class="ibo-spinner--icon fas fa-sync-alt fa-spin" aria-hidden="true"/></span>
HTML;
;
} elseif ($oExtension->bRemovedFromDisk) {
$sDisabledHtml = 'disabled=""';
$sChecked = 'checked';
} elseif (in_array($sCode, $this->aSelectedExtensionsForCheck)) {
$sChecked = 'checked';
}
$sVersion = $oExtension->sVersion;
$sIdEnable = "aExtensions[$sCode][enable]";
$aExtensions[] = [
<<<HTML
<input type="checkbox" $sDisabledHtml class="extension_check" $sChecked id="$sIdEnable" name="$sIdEnable"/>
HTML,
$sVersion,
$sLabel,
$sCode,
];
}
return $this->GetTableData('Extensions', $aColumns, $aExtensions);
}
private function GetTableData(string $sTableName, array $aColumns, array $aData): array
{
if (empty($aData)) {
return [
'Type' => 'Table',
'Columns' => [['label' => '']],
'Data' => [[ Dict::S('DataFeatureRemoval:Table:Empty')]],
];
}
$aNewColumns = [];
foreach ($aColumns as $sColumn) {
$aNewColumns[] = ['label' => Dict::S("DataFeatureRemoval:Table:$sTableName:$sColumn", Dict::S("DataFeatureRemoval:Column:$sColumn", $sColumn))];
}
$aColumns = $aNewColumns;
return [
'Type' => 'Table',
'Columns' => $aColumns,
'Data' => $aData,
];
}
/**
* @return void
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
*/
private function ValidateTransactionId(): void
{
if (empty($_POST)) {
return;
}
$sTransactionId = utils::ReadPostedParam('transaction_id', null, utils::ENUM_SANITIZATION_FILTER_TRANSACTION_ID);
IssueLog::Debug(__FUNCTION__.": Transaction [$sTransactionId]");
if (empty($sTransactionId) || !utils::IsTransactionValid($sTransactionId, false)) {
throw new DataFeatureRemovalException(Dict::S("iTopUpdate:Error:InvalidToken"));
}
}
/**
* @return array
*/
public function ReadRemovedExtensions(): array
{
if (count($this->aSelectedExtensionsForCheck) > 0) {
return $this->aSelectedExtensionsForCheck;
}
$aSelectedExtensionsFromUI = utils::ReadPostedParam('aExtensions', []);
foreach ($aSelectedExtensionsFromUI as $sCode => $aData) {
$sValue = $aData['enable'] ?? 'off';
if (($sValue) === 'on') {
$this->aSelectedExtensionsForCheck[] = $sCode;
}
}
// Add source removed to check
foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) {
if ($oExtension->bRemovedFromDisk) {
$this->aSelectedExtensionsForCheck[] = $sCode;
}
}
return $this->aSelectedExtensionsForCheck;
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Entity;
class DeletionPlanSummaryEntity
{
public string $sClass;
/**
* @var int : DEL_MANUAL|DEL_AUTO|DEL_SILENT|DEL_MOVEUP|DEL_NONE
* @see \AttributeDefinition DEL_xxx
*/
public int $iMode = 0;
public ?string $sIssue = null;
public int $iUpdateCount = 0;
public int $iDeleteCount = 0;
/**
* @param string $sClass
*/
public function __construct(string $sClass)
{
$this->sClass = $sClass;
}
}

View File

@@ -1,71 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Helper;
use Config;
use MetaModel;
use utils;
class DataFeatureRemovalConfig
{
private static DataFeatureRemovalConfig $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): DataFeatureRemovalConfig
{
if (!isset(static::$oInstance)) {
static::$oInstance = new DataFeatureRemovalConfig();
}
return static::$oInstance;
}
public function Get(string $sParamName, $default = null)
{
return MetaModel::GetModuleSetting(DataFeatureRemovalHelper::MODULE_NAME, $sParamName, $default);
}
public function GetBoolean(string $sParamName, $default = null): bool
{
$res = $this->Get($sParamName, $default);
return boolval($res);
}
public function IsEnabled(): bool
{
return $this->GetBoolean('enable', false);
}
public function Set(string $sParamName, $value)
{
$oConfig = utils::GetConfig();
$oConfig->SetModuleSetting(DataFeatureRemovalHelper::MODULE_NAME, $sParamName, $value);
}
/**
* @param \Config|null $oConfig
*
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
public function SaveItopConfiguration(Config $oConfig = null)
{
if (is_null($oConfig)) {
$oConfig = utils::GetConfig();
}
$sConfigFile = APPROOT.'conf/'.utils::GetCurrentEnvironment().'/config-itop.php';
@chmod($sConfigFile, 0770); // Allow overwriting the file
$oConfig->WriteToFile($sConfigFile);
@chmod($sConfigFile, 0444); // Read-only
}
}

View File

@@ -1,30 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Helper;
use Exception;
use Throwable;
class DataFeatureRemovalException extends Exception
{
public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null, array $aContext = [])
{
if (!is_null($previous)) {
$sStack = $previous->getTraceAsString();
$sError = $previous->getMessage();
} else {
$sStack = $this->getTraceAsString();
$sError = '';
}
$aContext['error'] = $sError;
$aContext['stack'] = $sStack;
DataFeatureRemovalLog::Error($message, null, $aContext);
parent::__construct($message, $code, $previous);
}
}

View File

@@ -1,23 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Helper;
class DataFeatureRemovalHelper
{
public const MODULE_NAME = 'combodo-data-feature-removal';
public static function IsTimeLimitExceeded(int $iUnixTimeLimit): bool
{
if ($iUnixTimeLimit === 0) {
//no time limit
return false;
}
return (time() > $iUnixTimeLimit);
}
}

View File

@@ -1,25 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Helper;
use LogAPI;
class DataFeatureRemovalLog extends LogAPI
{
public const CHANNEL_DEFAULT = 'DataFeatureRemoval';
protected static $m_oFileLog = null;
public static function Enable($sTargetFile = null)
{
if (empty($sTargetFile)) {
$sTargetFile = APPROOT.'log/error.log';
}
parent::Enable($sTargetFile);
}
}

View File

@@ -1,44 +0,0 @@
<?php
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalConfig;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Service\BackgroundOperationService;
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
class DataFeatureRemovalBackgroundTask implements iBackgroundProcess
{
/**
* @inheritDoc
*/
public function GetPeriodicity()
{
return DataFeatureRemovalConfig::GetInstance()->Get('cron_periodicity_in_s', 10);
}
/**
* @inheritDoc
*/
public function Process($iUnixTimeLimit)
{
while ($oBackgroundOperation = BackgroundOperationService::GetInstance()->GetNext()) {
$aClasses = BackgroundOperationService::GetInstance()->GetClasses($oBackgroundOperation);
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses, $iUnixTimeLimit);
IssueLog::Info(__METHOD__, null, $aRes);
IssueLog::Info(__METHOD__, null, [
'$iUnixTimeLimit' => $iUnixTimeLimit,
'time' => time(),
'timeout reached' => DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit),
]);
if (DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit)) {
//timeout reached
return;
}
//execution finished before timeout: nothing left to remove
$oBackgroundOperation->DBDelete();
}
}
}

View File

@@ -1,105 +0,0 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Service;
use DataFeatureRemovalBackgroundOperation;
use DBObjectSet;
use DBSearch;
use MetaModel;
class BackgroundOperationService
{
private static BackgroundOperationService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): BackgroundOperationService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new BackgroundOperationService();
}
return self::$oInstance;
}
final public static function SetInstance(?BackgroundOperationService $oInstance): void
{
static::$oInstance = $oInstance;
}
/**
* @return \DataFeatureRemovalBackgroundOperation
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @throws \OQLException
*/
public function GetNext(): ?DataFeatureRemovalBackgroundOperation
{
$sOQL = <<<OQL
SELECT DataFeatureRemovalBackgroundOperation
OQL;
$oSet = new DBObjectSet(DBSearch::FromOQL($sOQL), ['creation_date' => true]);
/** @var DataFeatureRemovalBackgroundOperation $oDBObject */
$oDBObject = $oSet->Fetch();
return $oDBObject;
}
/**
* @param array $aExtensionsCodes
* @param array $aClasses
* @return void
* @throws \ArchivedObjectException
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \CoreWarning
* @throws \MySQLException
* @throws \OQLException
*/
public function CreateOperation(array $aExtensionsCodes, array $aClasses): void
{
sort($aExtensionsCodes);
sort($aClasses);
$aValues = [
'creation_date' => time(),
'features_code' => '|'.implode('|', $aExtensionsCodes).'|',
'classes' => implode(',', $aClasses),
];
$oObj = MetaModel::NewObject('DataFeatureRemovalBackgroundOperation', $aValues);
$oObj->DBWrite();
}
/**
* @param string $sExtensionCode
* @return bool
* @throws \CoreException
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \OQLException
*/
public function IsExtensionBeingCleaned(string $sExtensionCode): bool
{
$sOQL = <<<OQL
SELECT DataFeatureRemovalBackgroundOperation WHERE features_code LIKE '%|$sExtensionCode|%'
OQL;
$oSet = new DBObjectSet(DBSearch::FromOQL($sOQL));
return $oSet->Count() > 0;
}
/**
* @param \DataFeatureRemovalBackgroundOperation $oBackgroundOperation
* @return array
*/
public function GetClasses(DataFeatureRemovalBackgroundOperation $oBackgroundOperation): array
{
return explode(',', $oBackgroundOperation->Get('classes'));
}
}

View File

@@ -1,80 +0,0 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Service;
use iTopExtension;
use iTopExtensionsMap;
use MetaModel;
class DataFeatureRemoverExtensionService
{
private static DataFeatureRemoverExtensionService $oInstance;
private array $aItopExtensions = [];
private array $aIncludingExtensionsByModuleName = [];
protected function __construct()
{
}
final public static function GetInstance(): DataFeatureRemoverExtensionService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new DataFeatureRemoverExtensionService();
}
return self::$oInstance;
}
final public static function SetInstance(?DataFeatureRemoverExtensionService $oInstance): void
{
self::$oInstance = $oInstance;
}
/**
* @param string $sModuleName
*
* @return array
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
*/
public function GetIncludingExtensions(string $sModuleName): array
{
if (count($this->aIncludingExtensionsByModuleName) === 0) {
foreach ($this->ReadItopExtensions() as $oExtension) {
$aModuleNames = $oExtension->aModules;
if (is_array($aModuleNames) && count($aModuleNames) > 0) {
foreach ($aModuleNames as $sModule) {
$aExtensions = $this->aIncludingExtensionsByModuleName[$sModule] ?? [];
$aExtensions[] = $oExtension->sLabel.'/'.$oExtension->sVersion;
$this->aIncludingExtensionsByModuleName[$sModule] = $aExtensions;
}
}
}
}
return $this->aIncludingExtensionsByModuleName[$sModuleName] ?? [];
}
/**
* @return iTopExtension[]
*/
public function ReadItopExtensions(): array
{
if (count($this->aItopExtensions) === 0) {
$oExtensionsMap = new iTopExtensionsMap();
$oExtensionsMap->LoadInstalledExtensionsFromDatabase(MetaModel::GetConfig());
$this->aItopExtensions = $oExtensionsMap->GetAllExtensionsToDisplayInSetup(true);
uasort($this->aItopExtensions, function (iTopExtension $oiTopExtension1, iTopExtension $oiTopExtension2) {
return strcmp($oiTopExtension1->sLabel, $oiTopExtension2->sLabel);
});
}
return $this->aItopExtensions;
}
}

View File

@@ -1,212 +0,0 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Service;
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use DBObjectSearch;
use DeletionPlan;
use MetaModel;
class DeletionPlanService
{
private static DeletionPlanService $oInstance;
public int $iExecutionCount = 0;
protected function __construct()
{
}
final public static function GetInstance(): DeletionPlanService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new DeletionPlanService();
}
return self::$oInstance;
}
final public static function SetInstance(?DeletionPlanService $oInstance): void
{
self::$oInstance = $oInstance;
}
/**
* Get a summary of the deletion plan computed for the classes.
* The result is used for display
*
* @param array|null $aClasses
*
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity>
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function GetDeletionPlanSummary(?array $aClasses): array
{
$aSummary = [];
if (is_null($aClasses)) {
return $aSummary;
}
$oDeletionPlan = $this->GetDeletionPlan($aClasses);
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aUpdates) {
$oDeletionPlanSummaryEntity = new DeletionPlanSummaryEntity($sClass);
$oDeletionPlanSummaryEntity->iUpdateCount = count($aUpdates);
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aDeletes) {
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
$oDeletionPlanSummaryEntity->iDeleteCount = count($aDeletes);
$aDelete = array_shift($aDeletes);
$oDeletionPlanSummaryEntity->iMode = $aDelete['mode'];
$oDeletionPlanSummaryEntity->sIssue = $aDelete['issue'] ?? null;
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
return $aSummary;
}
/**
* @param string $sClass
*
* @return \DBObject[]
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @throws \Exception
*/
private function GetAllObjects(string $sClass): array
{
$oFilter = new DBObjectSearch($sClass);
$oFilter->AllowAllData();
$oSet = new \DBObjectSet($oFilter);
return $oSet->ToArray();
}
/**
* @since 3.3.0
* @param array $aClasses
* @param int $iUnixTimeLimit : max execution time in seconds since Epoch before stopping deletion. by default: no limit (ie remove all without stop)
* @param int $iMaxExecutionCount : max execution count before stopping deletion. by default: no limit (ie remove all without stop)
*
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity>
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function ExecuteDeletionPlan(array $aClasses, int $iUnixTimeLimit = 0, int $iMaxExecutionCount = -1): array
{
$oDeletionPlan = $this->GetDeletionPlan($aClasses);
if (count($oDeletionPlan->GetIssues()) > 0) {
throw new DataFeatureRemovalException("Deletion Plan cannot be executed due to issues");
}
$this->iExecutionCount = 0;
$aSummary = [];
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) {
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
foreach ($aToUpdate as $aData) {
if ($this->IsTimeLimitExceeded($iUnixTimeLimit, $iMaxExecutionCount)) {
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
return $aSummary;
}
$this->iExecutionCount++;
$oToUpdate = $aData['to_reset'];
/** @var \DBObject $oToUpdate */
foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) {
$oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]);
}
$oToUpdate->DBUpdate();
$oDeletionPlanSummaryEntity->iUpdateCount++;
}
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aDeletes) {
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
foreach ($aDeletes as $sId => $aDelete) {
if ($this->IsTimeLimitExceeded($iUnixTimeLimit, $iMaxExecutionCount)) {
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
return $aSummary;
}
$this->iExecutionCount++;
try {
CMDBSource::Query('START TRANSACTION');
// Delete any existing change tracking about the current object
$oFilter = new DBObjectSearch('CMDBChangeOp');
$oFilter->AddCondition('objclass', $sClass, '=');
$oFilter->AddCondition('objkey', $sId, '=');
MetaModel::PurgeData($oFilter);
// Delete the entry
$aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false));
foreach ($aClassesToRemove as $sParentClass) {
$oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id");
$sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]);
CMDBSource::DeleteFrom($sQuery);
}
CMDBSource::Query('COMMIT');
} catch (\Exception $e) {
\IssueLog::Exception(__METHOD__.": Cleanup failed", $e);
CMDBSource::Query('ROLLBACK');
throw $e;
}
$oDeletionPlanSummaryEntity->iDeleteCount++;
}
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
return $aSummary;
}
/**
* Get a deletion plan for all the objects of the classes
*
* @param array $aClasses array of class names to clean
*
* @return \DeletionPlan
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function GetDeletionPlan(array $aClasses): DeletionPlan
{
$oDeletionPlan = new DeletionPlan();
foreach ($aClasses as $sClass) {
$aObjects = $this->GetAllObjects($sClass);
foreach ($aObjects as $oObject) {
$oObject->CheckToDelete($oDeletionPlan);
}
}
return $oDeletionPlan;
}
public function IsTimeLimitExceeded(int $iUnixTimeLimit, int $iMaxExecutionCount = -1): bool
{
if (($iMaxExecutionCount !== -1) && ($iMaxExecutionCount <= $this->iExecutionCount)) {
return true;
}
return DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit);
}
}

View File

@@ -1,31 +0,0 @@
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:DeletionPlan:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:DeletionPlan:SubTitle'|dict_s } %}
{% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %}
{% EndUIPanel %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %}
{% for sKey, sClass in aClasses %}
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
{% endfor %}
{% for sKey, sCode in aExtensionsCode %}
{% UIInput ForHidden { sName:"aExtensionsCode[" ~ sKey ~ "]", sValue:sCode } %}
{% endfor %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', sValue: 'immediate_deletion', bIsSubmit:true} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoAsyncDeletion'|dict_s, sName:'btn_deletion', sId:'btn_async_deletion', sValue: 'async_deletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:BackToMain'|dict_s, sName:'btn_back', sId:'btn_back', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}

View File

@@ -1,14 +0,0 @@
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:DoDeletion:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:DoDeletion:SubTitle'|dict_s } %}
{% UIDataTable ForForm { sRef:'aDeletionExecutionSummary', aColumns:aDeletionExecutionSummary.Columns, aData:aDeletionExecutionSummary.Data} %}{% EndUIDataTable %}
{% EndUIPanel %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:BackToMain'|dict_s, sName:'btn_back_to_main', sId:'btn_back_to_main', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}

View File

@@ -1,24 +0,0 @@
{# @copyright Copyright (C) 2010-2024 Combodo SAS #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% if bHasData %}
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Analysis:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Analysis:SubTitle'|dict_format(iCount) } %}
{% UIDataTable ForForm { sRef:'aAnalysisDataTable', aColumns:aAnalysisDataTable.Columns, aData:aAnalysisDataTable.Data} %}{% EndUIDataTable %}
{% EndUIPanel %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'DeletionPlan'} %}
{% for sKey, sClass in aClasses %}
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
{% endfor %}
{% for sKey, sCode in aExtensionsCode %}
{% UIInput ForHidden { sName:"aExtensionsCode[" ~ sKey ~ "]", sValue:sCode } %}
{% endfor %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:PlanDeletion'|dict_s, sName:'btn_plandeletion', sId:'btn_plandeletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% endif %}

View File

@@ -1,16 +0,0 @@
{# @copyright Copyright (C) 2010-2024 Combodo SAS #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% UIForm Standard {} %}
{% UIInput ForHidden {sName:'operation', sValue:'Analyze'} %}
{% UIInput ForHidden {sName:'transaction_id', sValue:sTransactionId} %}
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Features:Title'|dict_s} %}
{% UIDataTable ForForm { sRef:'aExtensions', aColumns:aExtensions.Columns, aData:aExtensions.Data} %}{% EndUIDataTable %}
{% EndUIFieldSet %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Analyze'|dict_s, sName:'btn_apply', sId:'btn_apply', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}

View File

@@ -1,37 +0,0 @@
{# @copyright Copyright (C) 2010-2025 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{# Usable variables: #}
{# * sTitle => page title #}
{# * sMessage => success message #}
{# * sError => error message #}
{# DataFeatureRemoval #}
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:Main:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Main:SubTitle'|dict_s } %}
{% UIAlert ForInformation { sTitle:'DataFeatureRemoval:Helper:Title'|dict_s } %}
{{ 'DataFeatureRemoval:Helper:Desc1'|dict_s }}<BR>
{{ 'DataFeatureRemoval:Helper:Desc2'|dict_s }}
{% EndUIAlert %}
{% if null != DataFeatureRemovalErrorMessage %}
<div id="feature_removal_error_msg_div" style="display:block">
{% UIAlert ForFailure { sTitle:'DataFeatureRemoval:Failure:Title'|dict_s, sId: 'feature_removal_error_msg', sContent:DataFeatureRemovalErrorMessage } %}
{% EndUIAlert %}
</div>
{% endif %}
{% include 'Features.html.twig' %}
{% include 'ExtensionRemovalData.html.twig' %}
{% if bAnalysisOk %}
{{ "DataFeatureRemoval:Analysis:Ok"|dict_s }}
{% UIToolbar ForButton {} %}
<a href="{{ sSetupUrl }}">
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:false} %}
</a>
{% EndUIToolbar %}
{% endif %}
{% EndUIPanel %}

View File

@@ -1,25 +0,0 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit4f96a7199e2c0d90e547333758b26464::getLoader();

View File

@@ -1,579 +0,0 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@@ -1,19 +0,0 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => $baseDir . '/src/Controller/DataFeatureRemovalController.php',
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanSummaryEntity' => $baseDir . '/src/Entity/DeletionPlanSummaryEntity.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => $baseDir . '/src/Helper/DataFeatureRemovalConfig.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => $baseDir . '/src/Helper/DataFeatureRemovalException.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => $baseDir . '/src/Helper/DataFeatureRemovalHelper.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => $baseDir . '/src/Helper/DataFeatureRemovalLog.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\BackgroundOperationService' => $baseDir . '/src/Service/BackgroundOperationService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => $baseDir . '/src/Service/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DeletionPlanService' => $baseDir . '/src/Service/DeletionPlanService.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View File

@@ -1,9 +0,0 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View File

@@ -1,11 +0,0 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Combodo\\iTop\\DataFeatureRemoval\\' => array($baseDir . '/src'),
'' => array($baseDir . '/src/NoNamespace'),
);

View File

@@ -1,37 +0,0 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit4f96a7199e2c0d90e547333758b26464
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit4f96a7199e2c0d90e547333758b26464', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit4f96a7199e2c0d90e547333758b26464', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit4f96a7199e2c0d90e547333758b26464::getInitializer($loader));
$loader->setClassMapAuthoritative(true);
$loader->register(true);
return $loader;
}
}

View File

@@ -1,50 +0,0 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
{
public static $prefixLengthsPsr4 = array (
'C' =>
array (
'Combodo\\iTop\\DataFeatureRemoval\\' => 32,
),
);
public static $prefixDirsPsr4 = array (
'Combodo\\iTop\\DataFeatureRemoval\\' =>
array (
0 => __DIR__ . '/../..' . '/src',
),
);
public static $fallbackDirsPsr4 = array (
0 => __DIR__ . '/../..' . '/src/NoNamespace',
);
public static $classMap = array (
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => __DIR__ . '/../..' . '/src/Controller/DataFeatureRemovalController.php',
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanSummaryEntity' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanSummaryEntity.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalConfig.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalException.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalHelper.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalLog.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\BackgroundOperationService' => __DIR__ . '/../..' . '/src/Service/BackgroundOperationService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => __DIR__ . '/../..' . '/src/Service/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DeletionPlanService' => __DIR__ . '/../..' . '/src/Service/DeletionPlanService.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$prefixDirsPsr4;
$loader->fallbackDirsPsr4 = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$fallbackDirsPsr4;
$loader->classMap = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$classMap;
}, null, ClassLoader::class);
}
}

View File

@@ -30,7 +30,7 @@ SetupWebPage::AddModule(
// Identification
//
'label' => 'Database maintenance tools',
'category' => 'business',
'category' => 'Application management',
// Setup
//

View File

@@ -10,7 +10,6 @@ namespace Combodo\iTop\DBTools\Service;
use CMDBSource;
use DBObjectSearch;
use DBObjectSet;
use IssueLog;
class DBToolsUtils
{

View File

@@ -219,7 +219,7 @@
<choice>
<extension_code>itop-problem-mgmt</extension_code>
<title>Problem Management</title>
<description>Select this option to track "Problems" in iTop.</description>
<description>Select this option track "Problems" in iTop.</description>
<modules type="array">
<module>itop-problem-mgmt</module>
</modules>

View File

@@ -28,6 +28,7 @@ class ConfigEditorController extends Controller
public function __construct()
{
parent::__construct(MODULESROOT.static::MODULE_NAME.'/templates', static::MODULE_NAME);
$this->SetDebugAllowed(false);
}
public function OperationEdit(): void

View File

@@ -30,7 +30,7 @@ SetupWebPage::AddModule(
// Identification
//
'label' => 'iTop Core Update',
'category' => 'business',
'category' => 'Application management',
// Setup
//

View File

@@ -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'
);

View File

@@ -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])) {

View File

@@ -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());
}

View File

@@ -1,17 +1,9 @@
<?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
*/
@@ -32,7 +24,6 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
/**
* Update the includes for the target environment
*
* @param Config $oConfig
*/
public function UpdateIncludes(Config $oConfig)
@@ -42,9 +33,7 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
/**
* 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)
@@ -68,10 +57,8 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
/**
* 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)

View File

@@ -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'];

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -306,11 +306,8 @@
<is_null_allowed>true</is_null_allowed>
</field>
<field id="servicesubcategory_id" xsi:type="AttributeExternalKey">
<filter><![CDATA[SELECT ServiceSubcategory WHERE service_id = :this->service_id AND (ISNULL(:this->request_type) OR request_type = :this->request_type) AND status != 'obsolete']]></filter>
<dependencies>
<attribute id="service_id"/>
<attribute id="request_type"/>
</dependencies>
<filter><![CDATA[SELECT ServiceSubcategory WHERE service_id = :this->service_id AND status != 'obsolete']]></filter>
<dependencies/>
<sql>servicesubcategory_id</sql>
<target_class>ServiceSubcategory</target_class>
<is_null_allowed>true</is_null_allowed>
@@ -1334,21 +1331,23 @@
// Compute the priority of the ticket
$this->Set('priority', $this->ComputePriority());
// Compute the request_type if not already defined (by the user)
$sType = $this->Get('request_type');
if (is_null($sType) || ($sType === ''))
{
$iSvcSubcat = $this->Get('servicesubcategory_id');
if ($iSvcSubcat != 0)
{
$oSvcSubcat = MetaModel::GetObject(ServiceSubcategory::class, $iSvcSubcat, true, true);
$this->Set('request_type', $oSvcSubcat->Get('request_type'));
}
}
return parent::ComputeValues();
}]]></code>
</method>
<method id="EvtComputeRequestType">
<static>false</static>
<access>public</access>
<type>EventListener</type>
<code><![CDATA[ public function EvtComputeRequestType(?Combodo\iTop\Service\Events\EventData $oEventData = null)
{
$iSvcSubcat = $this->Get('servicesubcategory_id');
if ($iSvcSubcat != 0)
{
$oSvcSubcat = MetaModel::GetObject(ServiceSubcategory::class, $iSvcSubcat, true, true);
$this->Set('request_type', $oSvcSubcat->Get('request_type'));
}
}]]></code>
</method>
<method id="DisplayBareRelations">
<static>false</static>
<access>public</access>
@@ -1528,6 +1527,13 @@
}]]></code>
</method>
</methods>
<event_listeners>
<event_listener id="EVENT_DB_BEFORE_WRITE">
<event>EVENT_DB_BEFORE_WRITE</event>
<callback>EvtComputeRequestType</callback>
<rank>0</rank>
</event_listener>
</event_listeners>
<presentation>
<details>
<items>

View File

@@ -12,7 +12,8 @@ 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',
'itop-portal/3.0.0', // module_design_itop_design->module_designs->itop-portal
],
'mandatory' => false,
'visible' => true,

File diff suppressed because it is too large Load Diff

View File

@@ -1279,6 +1279,7 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
'UI:DashletGroupBy:Prop-GroupBy:DayOfMonth' => 'Jour du mois pour %1$s',
'UI:DashletGroupBy:Prop-GroupBy:Select-Hour' => '%1$s (heure)',
'UI:DashletGroupBy:Prop-GroupBy:Select-Month' => '%1$s (mois)',
'UI:DashletGroupBy:Prop-GroupBy:Select-Year' => '%1$s (année)',
'UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek' => '%1$s (jour de la semaine)',
'UI:DashletGroupBy:Prop-GroupBy:Select-DayOfMonth' => '%1$s (jour du mois)',
'UI:DashletGroupBy:MissingGroupBy' => 'Veuillez sélectionner le champ sur lequel les objets seront groupés',
@@ -1544,7 +1545,8 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
'UI:Search:Criteria:HierarchicalKey:ChildrenIncluded:Hint' => 'Les descendants des objets sélectionnés seront inclus.',
'UI:Search:Criteria:Raw:Filtered' => 'Filtré',
'UI:Search:Criteria:Raw:FilteredOn' => 'Filtré sur %1$s',
'UI:StateChanged' => 'Etat modifié',
'UI:StateChanged' => 'État modifié',
'UI:AddSubTree' => 'Ajouter une entrée',
]);
//

View File

@@ -19,8 +19,15 @@
*/
// Input
Dict::Add('EN US', 'English', 'English', [
'UI:Component:Input:ChangeNotAllowed' => 'This change is not allowed',
'UI:Component:Input:Password:DoesNotMatch' => 'Passwords do not match',
'UI:Component:Input:Set:MinimumItems' => 'Minimum %1$s item(s) required',
]);
Dict::Add(
'EN US',
'English',
'English',
[
'UI:Component:Input:ChangeNotAllowed' => 'This change is not allowed',
'UI:Component:Input:Password:DoesNotMatch' => 'Passwords do not match',
'UI:Component:Input:Set:MinimumItems' => 'Minimum %1$s item(s) required',
'UI:Component:Input:Select:Select_item' => 'Select an item...',
]
);

View File

@@ -10,8 +10,15 @@
/**
*
*/
Dict::Add('FR FR', 'French', 'Français', [
'UI:Component:Input:ChangeNotAllowed' => 'Cette modification n\'est pas autorisée',
'UI:Component:Input:Password:DoesNotMatch' => 'Les mots de passe ne correspondent pas',
'UI:Component:Input:Set:MinimumItems' => 'Minimum %1$s élément(s) requis',
]);
Dict::Add(
'FR FR',
'French',
'Français',
[
'UI:Component:Input:ChangeNotAllowed' => 'Cette modification n\'est pas autorisée',
'UI:Component:Input:Password:DoesNotMatch' => 'Les mots de passe ne correspondent pas',
'UI:Component:Input:Set:MinimumItems' => 'Minimum %1$s élément(s) requis',
'UI:Component:Input:Select:Select_item' => 'Sélectionnez un élément...',
]
);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('CS CZ', 'Czech', 'Čeština', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('DA DA', 'Danish', 'Dansk', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('DE DE', 'German', 'Deutsch', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,21 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
Dict::Add('EN US', 'English', 'English', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('FR FR', 'French', 'Français', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installé',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'va être installé',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'pas installé',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'va être désinstallé',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'non désinstallable',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'supprimé du disque',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'À propos de %1$s',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'Plus d\'informations',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Forcer la désinstallation',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('HU HU', 'Hungarian', 'Magyar', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('IT IT', 'Italian', 'Italiano', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

View File

@@ -1,23 +0,0 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
*
*/
Dict::Add('JA JP', 'Japanese', '日本語', [
'UI:Layout:ExtensionsDetails:BadgeInstalled' => 'installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeInstalled' => 'to be installed~~',
'UI:Layout:ExtensionsDetails:BadgeNotInstalled' => 'not installed~~',
'UI:Layout:ExtensionsDetails:BadgeToBeUninstalled' => 'to be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeNotUninstallable' => 'cannot be uninstalled~~',
'UI:Layout:ExtensionsDetails:BadgeMissingFromDisk' => 'missing from disk~~',
'UI:Layout:ExtensionsDetails:MenuAboutTitle' => 'About %1$s~~',
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
]);

Some files were not shown because too many files have changed in this diff Show More