Compare commits

..

4 Commits

Author SHA1 Message Date
Eric Espie
a7517298c5 N°9617 - Send EVENT on data export for traceability - After Review 2026-07-09 15:49:45 +02:00
Eric Espie
7663be6e6e N°9617 - Send EVENT on data export for traceability 2026-07-09 15:37:52 +02:00
Eric Espie
574b7579c8 N°9617 - Send events on data export for traceability 2026-07-09 14:15:45 +02:00
Eric Espie
6a5f83e767 N°9617 - Send events on data export for traceability
# Conflicts:
#	core/restservices.class.inc.php
2026-07-09 14:15:41 +02:00
613 changed files with 5836 additions and 22130 deletions

View File

@@ -2,17 +2,13 @@
```mermaid
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'themeVariables': {
'git0': '#7ccf00',
'git1': '#7ccf00',
'git2': '#99a1af',
'git3': '#99a1af',
'git4': '#99a1af',
'git5': '#99a1af',
'git6': '#99a1af',
'git7': '#99a1af',
'git8': '#99a1af',
'git9': '#99a1af',
'git10': '#99a1af'
'git0': 'lawngreen',
'git3': 'dodgerblue',
'git4': 'grey',
'git5': 'grey',
'git6': 'grey',
'git7': 'grey',
'git8': 'grey'
}, 'gitGraph': {'showBranches': true,'mainBranchName': 'develop','rotateCommitLabel': true}} }%%
gitGraph
commit id: "2016-07-06" tag: "2.3.0" type: HIGHLIGHT
@@ -110,11 +106,6 @@ gitGraph
commit id: "2025-09-25" tag: "2.7.13"
checkout support/3.2
commit id: "2026-04-27 " tag: "3.2.3"
checkout support/3.2.3
commit id: "2026-05-25 " tag: "3.2.3-1"
commit id: "2026-07-17 " tag: "3.2.3-2"
checkout develop
commit id: "2026-07-23" tag: "3.3.0-beta1"
```
To learn more, check the [iTop community versions history on the official wiki](https://www.itophub.io/wiki/page?id=latest:release:start).

View File

@@ -8,7 +8,6 @@ body:
Please explain why you're creating this issue :
- Please describe what's your improvement proposition.
- Then tell us if you're willing to create a PR for this enhancement ? If so, we'll indicate in the issue if we're interested in it.
- The more users are affected and the greater is the impact, the more likely this improvement will be accepted. If you identified a bug and have an improvement proposition, please use the 'Bug report' template instead."
- type: textarea
id: enhancement_details

View File

@@ -1,7 +1,6 @@
<!--
IMPORTANT: Before creating your PR, please create an issue first to know if Combodo is interested in your contribution (not needed for translations PR).
Since we may refuse a PR, it's preferable to create an issue first, to avoid spending time coding something that won't be accepted.
PR that will benefit the most users and has a greater impact on the product are more likely to be accepted, so please explain your use case and why you think it would be useful for the community.
Once you've done it, and we confirmed we're interested in it, please follow the guidelines within this PR template before submitting it, it will greatly help us process your PR. 🙏
@@ -10,10 +9,10 @@ Any PRs not following the guidelines or with missing information will not be con
## Base information
| Question | Answer |
|---------------------------------------------------------------------------------|---------------------------------------|
| Related to a SourceForge thread / Another PR / A GitHub Issue / Combodo ticket? | <!-- Put the URL or ticket number --> |
| Type of change? | Bug fix / Enhancement / Translations |
| Question | Answer |
|---------------------------------------------------------------------------------|--------------------------------------|
| Related to a SourceForge thread / Another PR / A GitHub Issue / Combodo ticket? | <!-- Put the URL --> |
| Type of change? | Bug fix / Enhancement / Translations |
## Symptom (bug) / Objective (enhancement)
@@ -71,7 +70,6 @@ Explain in details how you are proposing to solve this:
Don't remove these lines, check them once done.
-->
- [ ] If my PR is a bug fix or an enhancement, I opened an issue that got reviewed and approved by Combodo
- [ ] I have performed a self-review of my code
- [ ] I have tested all changes I made on an iTop instance
- [ ] I have added a unit test, otherwise I have explained why I couldn't

0
.make/license/gen-community-license.sh Executable file → Normal file
View File

0
.make/license/sortLicenceXml.php Executable file → Normal file
View File

0
.make/license/updateLicenses.php Executable file → Normal file
View File

0
.make/release/changelog.php Executable file → Normal file
View File

0
.make/release/update-versions.php Executable file → Normal file
View File

0
.make/release/update-xml.php Executable file → Normal file
View File

0
.make/release/update.classes.inc.php Executable file → Normal file
View File

View File

@@ -582,9 +582,7 @@ class UserRightsProfile extends UserRightsAddOnAPI
*/
public function ListProfiles($oUser)
{
if (!array_key_exists('profile_list', $oUser->ListChanges())) {
// Profiles list is not modified on the $oUser object, we can use DBObjectSearch with `all data` capabilities
// Note: This is the default behavior
if (count($oUser->ListChanges()) === 0) { // backward compatibility
$aRet = [];
$oSearch = new DBObjectSearch('URP_UserProfile');
$oSearch->AllowAllData();
@@ -597,8 +595,6 @@ class UserRightsProfile extends UserRightsAddOnAPI
return $aRet;
} else {
// Profiles list must be computed with memory changes.
// Note: this is a bit tricky because the user object may have been modified in memory (e.g. a profile added or removed) and we need to take that into account
$aRet = [];
$oProfilesSet = $oUser->Get('profile_list');
foreach ($oProfilesSet as $oUserProfile) {

View File

@@ -30,15 +30,6 @@ require_once(APPROOT.'application/newsroomprovider.class.inc.php');
* @license http://opensource.org/licenses/AGPL-3.0
* @since 2.7.0
*/
require_once(APPROOT.'application/applicationextension/common/ApplicationPopupMenuItem.php');
require_once(APPROOT.'application/applicationextension/common/iPopupMenuExtension.php');
require_once(APPROOT.'application/applicationextension/common/JSButtonItem.php');
require_once(APPROOT.'application/applicationextension/common/JSPopupMenuItem.php');
require_once(APPROOT.'application/applicationextension/common/SeparatorPopupMenuItem.php');
require_once(APPROOT.'application/applicationextension/common/URLButtonItem.php');
require_once(APPROOT.'application/applicationextension/common/URLPopupMenuItem.php');
require_once(APPROOT.'application/applicationextension/backoffice/iApplicationUIExtension.php');
require_once(APPROOT.'application/applicationextension/backoffice/iBackofficeDictEntriesExtension.php');
require_once(APPROOT.'application/applicationextension/backoffice/iBackofficeDictEntriesPrefixesExtension.php');
@@ -52,12 +43,19 @@ require_once(APPROOT.'application/applicationextension/backoffice/iBackofficeScr
require_once(APPROOT.'application/applicationextension/backoffice/iBackofficeStyleExtension.php');
require_once(APPROOT.'application/applicationextension/backoffice/iFieldRendererMappingsExtension.php');
require_once(APPROOT.'application/applicationextension/backoffice/iPageUIBlockExtension.php');
require_once(APPROOT.'application/applicationextension/backoffice/iPopupMenuExtension.php');
require_once(APPROOT.'application/applicationextension/backoffice/iPreferencesExtension.php');
require_once(APPROOT.'application/applicationextension/backoffice/iWelcomePopupExtension.php');
require_once(APPROOT.'application/applicationextension/backoffice/AbstractApplicationUIExtension.php');
require_once(APPROOT.'application/applicationextension/backoffice/AbstractPageUIBlockExtension.php');
require_once(APPROOT.'application/applicationextension/backoffice/AbstractPreferencesExtension.php');
require_once(APPROOT.'application/applicationextension/backoffice/AbstractWelcomePopupExtension.php');
require_once(APPROOT.'application/applicationextension/backoffice/ApplicationPopupMenuItem.php');
require_once(APPROOT.'application/applicationextension/backoffice/JSButtonItem.php');
require_once(APPROOT.'application/applicationextension/backoffice/JSPopupMenuItem.php');
require_once(APPROOT.'application/applicationextension/backoffice/SeparatorPopupMenuItem.php');
require_once(APPROOT.'application/applicationextension/backoffice/URLButtonItem.php');
require_once(APPROOT.'application/applicationextension/backoffice/URLPopupMenuItem.php');
require_once(APPROOT.'application/applicationextension/iBackupExtraFilesExtension.php');
require_once(APPROOT.'application/applicationextension/iKPILoggerExtension.php');

View File

@@ -1,9 +1,5 @@
<?php
use Combodo\iTop\Application\UI\Base\Common\Metadata\AriaAttributes;
use Combodo\iTop\Application\UI\Base\Common\Metadata\DataAttributes;
use Combodo\iTop\Application\UI\Base\Common\Metadata\Grouping;
/**
* Base class for the various types of custom menus
*
@@ -11,7 +7,7 @@ use Combodo\iTop\Application\UI\Base\Common\Metadata\Grouping;
* @package UIExtensibilityAPI
* @since 2.0
*/
abstract class ApplicationPopupMenuItem implements iGroupingProvider, iDataAttributesProvider, iAriaAttributesProvider
abstract class ApplicationPopupMenuItem
{
/** @ignore */
protected $sUID;
@@ -23,12 +19,6 @@ abstract class ApplicationPopupMenuItem implements iGroupingProvider, iDataAttri
protected $sIconClass;
/** @ignore */
protected $aCssClasses;
/** @since 3.3.0 */
protected ?AriaAttributes $oAriaAttributes = null;
/** @since 3.3.0 */
protected ?DataAttributes $oDataAttributes = null;
/** @since 3.3.0 */
protected ?Grouping $oGrouping = null;
/**
* Constructor
@@ -68,17 +58,6 @@ abstract class ApplicationPopupMenuItem implements iGroupingProvider, iDataAttri
return $this->sLabel;
}
/**
* @param string $sLabel
*
* @api
* @since 3.3.0
*/
public function SetLabel(string $sLabel): void
{
$this->sLabel = $sLabel;
}
/**
* Get the CSS classes
*
@@ -162,76 +141,9 @@ abstract class ApplicationPopupMenuItem implements iGroupingProvider, iDataAttri
*/
abstract public function GetMenuItem();
/**
* Get additional data, child classes can use this without having to worry about this class interfaces
* @return array<string, mixed>
* @since 3.3.0
*/
protected function GetMenuItemAdditionalData(): array
{
$aAdditionalData = [];
$aDataAttributes = $this->GetDataAttributes()->GetAttributes();
if (!empty($aDataAttributes)) {
$aAdditionalData['data_attributes'] = $aDataAttributes;
}
$aAriaAttributes = $this->GetAriaAttributes()->GetAttributes();
if (!empty($aAriaAttributes)) {
$aAdditionalData['aria_attributes'] = $aAriaAttributes;
}
$oGrouping = $this->GetGrouping();
if ($oGrouping !== null) {
$aAdditionalData['grouping_uid'] = $oGrouping->GetUID();
}
return $aAdditionalData;
}
/** @ignore */
public function GetLinkedScripts()
{
return [];
}
/**
* @api
* @since 3.3.0
*/
public function GetAriaAttributes(): AriaAttributes
{
return $this->oAriaAttributes ??= new AriaAttributes();
}
/**
* @api
* @since 3.3.0
*/
public function GetDataAttributes(): DataAttributes
{
return $this->oDataAttributes ??= new DataAttributes();
}
/**
* @api
* @since 3.3.0
*/
public function GetGrouping(): ?Grouping
{
return $this->oGrouping;
}
/**
* @param Grouping|null $oGrouping
* @return static
* @api
* @since 3.3.0
*/
public function SetGrouping(?Grouping $oGrouping): static
{
$this->oGrouping = $oGrouping;
return $this;
}
}

View File

@@ -3,7 +3,7 @@
/**
* Class for adding an item into a popup menu that triggers some Javascript code
*
* Note: This works only in the backoffice, {@see \common\JSButtonItem} for the end-user portal
* Note: This works only in the backoffice, {@see \JSButtonItem} for the end-user portal
*
* @api
* @package UIExtensibilityAPI
@@ -40,7 +40,7 @@ class JSPopupMenuItem extends ApplicationPopupMenuItem
public function GetMenuItem()
{
// Note: the semicolumn is a must here!
$aData = [
return [
'label' => $this->GetLabel(),
'onclick' => $this->GetJsCode().'; return false;',
'url' => $this->GetUrl(),
@@ -48,8 +48,6 @@ class JSPopupMenuItem extends ApplicationPopupMenuItem
'icon_class' => $this->sIconClass,
'tooltip' => $this->sTooltip,
];
return array_merge($aData, $this->GetMenuItemAdditionalData());
}
/** @ignore */

View File

@@ -24,12 +24,6 @@ class SeparatorPopupMenuItem extends ApplicationPopupMenuItem
/** @ignore */
public function GetMenuItem()
{
$aData = [
'label' => '<hr class="menu-separator">',
'url' => '',
'css_classes' => $this->aCssClasses,
];
return array_merge($aData, $this->GetMenuItemAdditionalData());
return ['label' => '<hr class="menu-separator">', 'url' => '', 'css_classes' => $this->aCssClasses];
}
}

View File

@@ -3,7 +3,7 @@
/**
* Class for adding an item into a popup menu that browses to the given URL
*
* Note: This works only in the backoffice, {@see \common\URLButtonItem} for the end-user portal
* Note: This works only in the backoffice, {@see \URLButtonItem} for the end-user portal
*
* @api
* @package UIExtensibilityAPI
@@ -35,15 +35,13 @@ class URLPopupMenuItem extends ApplicationPopupMenuItem
/** @ignore */
public function GetMenuItem()
{
$aData = ['label' => $this->GetLabel(),
return ['label' => $this->GetLabel(),
'url' => $this->GetUrl(),
'target' => $this->GetTarget(),
'css_classes' => $this->aCssClasses,
'icon_class' => $this->sIconClass,
'tooltip' => $this->sTooltip,
];
return array_merge($aData, $this->GetMenuItemAdditionalData());
}
/** @ignore */

View File

@@ -1,7 +1,7 @@
<?php
/**
* Implement this interface to add sass files (SCSS) to the backoffice pages.
* Implement this interface to add sass file (SCSS) to the backoffice pages.
* example: return "css/setup.scss"
*
* @api
@@ -11,9 +11,9 @@
interface iBackofficeSassExtension
{
/**
* @return array An array of relative paths (from loaded import paths) to the files to compile and include
* @see \iTopWebPage::$a_linked_stylesheets
* @return string
* @see \iTopWebPage::$a_styles
* @api
*/
public function GetSassRelPaths(): array;
public function GetSass(): string;
}

View File

@@ -95,24 +95,7 @@ interface iPopupMenuExtension
* @todo
*/
public const PORTAL_MENU_ACTIONS = 10;
/**
* Get the list of items to be added to the backoffice top-bar items
* $param is an array: [] for now, in a distant future we want to give a complete context
* @since 3.3.0
*/
public const MENU_TOPBAR_ACTIONS = 11;
/**
* Get the list of items to be added to the actions on a given form field (i.e. the attribute of an object)
* $param is an array: ['object' => DBObject, att_code => attribute code , mode => edit/read]
* @since 3.3.0
*/
public const MENU_OBJDETAILS_FIELD_ACTIONS = 12;
/**
* Get the list of items to be added to the actions in the activity panel (read-only mode)
* $param is an array: ['object' => DBObject, caselog_att_code => caselog attribute code or 'activity']
* @since 3.3.0
*/
public const MENU_OBJDETAILS_ACTIVITY_PANEL_ACTIONS = 13;
/**
* Get the list of items to be added to a menu.
*

View File

@@ -1,15 +0,0 @@
<?php
use Combodo\iTop\Application\UI\Base\Common\Metadata\AriaAttributes;
/**
* Allows to provide ARIA attributes to classes that implement this interface.
* The ARIA attributes will be used to improve accessibility of the UI components
*
* @api
* @since 3.3.0
*/
interface iAriaAttributesProvider
{
public function GetAriaAttributes(): AriaAttributes;
}

View File

@@ -1,14 +0,0 @@
<?php
use Combodo\iTop\Application\UI\Base\Common\Metadata\DataAttributes;
/**
* Allows to provide data attributes to classes that implement this interface.
* The data attributes will be used to provide additional information to the UI components
* @api
* @since 3.3.0
*/
interface iDataAttributesProvider
{
public function GetDataAttributes(): DataAttributes;
}

View File

@@ -1,13 +0,0 @@
<?php
use Combodo\iTop\Application\UI\Base\Common\Metadata\Grouping;
/**
* Allows to provide grouping information to classes that implement this interface.
* @api
* @since 3.3.0
*/
interface iGroupingProvider
{
public function GetGrouping(): ?Grouping;
}

View File

@@ -1,14 +0,0 @@
<?php
/**
* Implemented by metadata value objects that flatten to HTML attributes.
* Ease the implementation of classes that need to provide HTML attributes to the UI components
*
* @api
* @since 3.3.0
*/
interface iHtmlAttributesSource
{
/** @return array<string, string> Attribute name => value, ready to be escaped and printed */
public function GetHtmlAttributes(): array;
}

View File

@@ -49,7 +49,6 @@ use Combodo\iTop\Application\WebPage\WebPage;
use Combodo\iTop\Renderer\BlockRenderer;
use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer;
use Combodo\iTop\Renderer\Console\ConsoleFormRenderer;
use Combodo\iTop\Service\InterfaceDiscovery\InterfaceDiscovery;
use Combodo\iTop\Service\Links\LinkSetDataTransformer;
use Combodo\iTop\Service\Links\LinkSetModel;
use Combodo\iTop\Service\TemporaryObjects\TemporaryObjectHelper;
@@ -1047,7 +1046,7 @@ HTML
// Add extra data for markup generation
// - Attribute code and AttributeDef. class
$val['attcode'] = $sAttCode;
$val['atttype'] = $oAttDef->GetTypeShortClassName();
$val['atttype'] = $oAttDef->GetType();
$val['attlabel'] = $sAttLabel;
$val['attflags'] = ($bEditMode) ? $this->GetFormAttributeFlags($sAttCode) : OPT_ATT_READONLY;
@@ -1066,14 +1065,6 @@ HTML
}
}
$val['value_raw'] = ($bExcludeRawValue === false) ? $this->Get($sAttCode) : '';
$val['actions'] = [];
/** @var \iPopupMenuExtension $oExtensionInstance */
foreach (InterfaceDiscovery::GetInstance()->FindItopClasses('iPopupMenuExtension') as $oExtensionInstance) {
foreach ($oExtensionInstance::EnumItems(iPopupMenuExtension::MENU_OBJDETAILS_FIELD_ACTIONS, ['object' => $this, 'att_code' => $sAttCode, 'mode' => $bEditMode ? 'edit' : 'read']) as $oMenuItem) {
$val['actions'][] = $oMenuItem;
}
}
// The field is visible, add it to the current column
$oField = FieldUIBlockFactory::MakeFromParams($val);
@@ -1544,6 +1535,190 @@ HTML
return $sHtml;
}
/**
* @param WebPage $oPage
* @param \CMDBObjectSet $oSet
* @param array $aParams
*
* @throws \Exception
* only used in old and deprecated export.php
*
* @internal Only to be used by `/webservices/export.php` : this is a legacy method that produces wrong HTML (no TR on table body rows)
*/
public static function DisplaySetAsHTMLSpreadsheet(WebPage $oPage, CMDBObjectSet $oSet, $aParams = [])
{
$oPage->add(self::GetSetAsHTMLSpreadsheet($oSet, $aParams));
}
/**
* Spreadsheet output: designed for end users doing some reporting
* Then the ids are excluded and replaced by the corresponding friendlyname
*
* @param \DBObjectSet $oSet
* @param array $aParams
*
* @return string
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \Exception
*
* @internal Only to be used by `/webservices/export.php` : this is a legacy method that produces wrong HTML (no TR on table body rows)
*/
public static function GetSetAsHTMLSpreadsheet(DBObjectSet $oSet, $aParams = [])
{
$aFields = null;
if (isset($aParams['fields']) && (strlen($aParams['fields']) > 0)) {
$aFields = explode(',', $aParams['fields']);
}
$bFieldsAdvanced = false;
if (isset($aParams['fields_advanced'])) {
$bFieldsAdvanced = (bool)$aParams['fields_advanced'];
}
$bLocalize = true;
if (isset($aParams['localize_values'])) {
$bLocalize = (bool)$aParams['localize_values'];
}
$aList = [];
$aClasses = $oSet->GetFilter()->GetSelectedClasses();
$aAuthorizedClasses = [];
foreach ($aClasses as $sAlias => $sClassName) {
if (UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) != UR_ALLOWED_NO) {
$aAuthorizedClasses[$sAlias] = $sClassName;
}
}
$aHeader = [];
foreach ($aAuthorizedClasses as $sAlias => $sClassName) {
$aList[$sAlias] = [];
foreach (MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef) {
if (is_null($aFields) || (count($aFields) == 0)) {
// Standard list of attributes (no link sets)
if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField())) {
$sAttCodeEx = $oAttDef->IsExternalField() ? $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode() : $sAttCode;
$aList[$sAlias][$sAttCodeEx] = $oAttDef;
if ($bFieldsAdvanced && $oAttDef->IsExternalKey(EXTKEY_RELATIVE)) {
$sRemoteClass = $oAttDef->GetTargetClass();
foreach (MetaModel::GetReconcKeys($sRemoteClass) as $sRemoteAttCode) {
$aList[$sAlias][$sAttCode.'->'.$sRemoteAttCode] = MetaModel::GetAttributeDef(
$sRemoteClass,
$sRemoteAttCode
);
}
}
}
} else {
// User defined list of attributes
if (in_array($sAttCode, $aFields) || in_array($sAlias.'.'.$sAttCode, $aFields)) {
$aList[$sAlias][$sAttCode] = $oAttDef;
}
}
}
// Replace external key by the corresponding friendly name (if not already in the list)
foreach ($aList[$sAlias] as $sAttCode => $oAttDef) {
if ($oAttDef->IsExternalKey()) {
unset($aList[$sAlias][$sAttCode]);
$sFriendlyNameAttCode = $sAttCode.'_friendlyname';
if (!array_key_exists(
$sFriendlyNameAttCode,
$aList[$sAlias]
) && MetaModel::IsValidAttCode($sClassName, $sFriendlyNameAttCode)) {
$oFriendlyNameAtt = MetaModel::GetAttributeDef($sClassName, $sFriendlyNameAttCode);
$aList[$sAlias][$sFriendlyNameAttCode] = $oFriendlyNameAtt;
}
}
}
foreach ($aList[$sAlias] as $sAttCodeEx => $oAttDef) {
$sColLabel = $bLocalize ? MetaModel::GetLabel($sClassName, $sAttCodeEx) : $sAttCodeEx;
$oFinalAttDef = $oAttDef->GetFinalAttDef();
if (get_class($oFinalAttDef) == 'AttributeDateTime') {
$aHeader[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Date').')';
$aHeader[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Time').')';
} else {
$aHeader[] = $sColLabel;
}
}
}
$sHtml = "<table border=\"1\">\n";
$sHtml .= "<tr>\n";
$sHtml .= "<td>".implode("</td><td>", $aHeader)."</td>\n";
$sHtml .= "</tr>\n";
$oSet->Seek(0);
while ($aObjects = $oSet->FetchAssoc()) {
$aRow = [];
foreach ($aAuthorizedClasses as $sAlias => $sClassName) {
$oObj = $aObjects[$sAlias];
foreach ($aList[$sAlias] as $sAttCodeEx => $oAttDef) {
if (is_null($oObj)) {
$aRow[] = '<td></td>';
} else {
$oFinalAttDef = $oAttDef->GetFinalAttDef();
if (get_class($oFinalAttDef) == 'AttributeDateTime') {
$sDate = $oObj->Get($sAttCodeEx);
if ($sDate === null) {
$aRow[] = '<td></td>';
$aRow[] = '<td></td>';
} else {
$iDate = AttributeDateTime::GetAsUnixSeconds($sDate);
$aRow[] = '<td>'.date(
'Y-m-d',
$iDate
).'</td>'; // Format kept as-is for 100% backward compatibility of the exports
$aRow[] = '<td>'.date(
'H:i:s',
$iDate
).'</td>'; // Format kept as-is for 100% backward compatibility of the exports
}
} else {
if ($oAttDef instanceof AttributeCaseLog) {
$rawValue = $oObj->Get($sAttCodeEx);
$outputValue = str_replace(
"\n",
"<br/>",
utils::EscapeHtml($rawValue->__toString())
);
// Trick for Excel: treat the content as text even if it begins with an equal sign
$aRow[] = '<td x:str>'.$outputValue.'</td>';
} else {
$rawValue = $oObj->Get($sAttCodeEx);
// Due to custom formatting rules, empty friendlynames may be rendered as non-empty strings
// let's fix this and make sure we render an empty string if the key == 0
if ($oAttDef instanceof AttributeExternalField && $oAttDef->IsFriendlyName()) {
$sKeyAttCode = $oAttDef->GetKeyAttCode();
if ($oObj->Get($sKeyAttCode) == 0) {
$rawValue = '';
}
}
if ($bLocalize) {
$outputValue = utils::EscapeHtml($oFinalAttDef->GetEditValue($rawValue));
} else {
$outputValue = utils::EscapeHtml($rawValue);
}
$aRow[] = '<td>'.$outputValue.'</td>';
}
}
}
}
}
$sHtml .= implode("\n", $aRow);
$sHtml .= "</tr>\n";
}
$sHtml .= "</table>\n";
return $sHtml;
}
/**
* @param WebPage $oPage
* @param \CMDBObjectSet $oSet
@@ -3075,12 +3250,8 @@ EOF
}
$aAllowedValues = MetaModel::GetAllowedValues_att($sClass, $sAttCode, $aArgs);
if (is_array($aAllowedValues) && count($aAllowedValues) == 1) {
// linkset are ignored
// N°9622 - Error in the user story life cycle
if (!$oAttDef->IsLinkSet()) {
$aValues = array_keys($aAllowedValues);
$this->Set($sAttCode, $aValues[0]);
}
$aValues = array_keys($aAllowedValues);
$this->Set($sAttCode, $aValues[0]);
}
}
}
@@ -3996,27 +4167,6 @@ HTML;
$sTagSetJson = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null, 'raw_data');
if ($sTagSetJson !== null) { // bulk modify, direct linked set not handled
$value = json_decode($sTagSetJson, true);
if ($this->IsNew()) {
if (is_array($value['orig_value'])) {
foreach ($value['orig_value'] as $val) {
if (!in_array($val, $value['removed'])) {
$value['added'][] = $val;
}
}
}
} else {
$aCurrentValues = $this->Get($sAttCode)->GetValues();
foreach ($value['orig_value'] as $val) {
if (!in_array($val, $aCurrentValues) && !in_array($val, $value['removed']) && !in_array($val, $value['added'])) {
$value['added'][] = $val;
}
}
foreach ($aCurrentValues as $val) {
if (!in_array($val, $value['orig_value']) && !in_array($val, $value['removed']) && !in_array($val, $value['added'])) {
$value['removed'][] = $val;
}
}
}
}
break;
@@ -4357,7 +4507,7 @@ HTML;
$oDivField = FieldUIBlockFactory::MakeLarge("");
// UIContentBlockUIBlockFactory::MakeStandard(null,["field_container field_large"]);
$oDivField->AddDataAttribute("attribute-type", $oAttDef->GetTypeShortClassName());
$oDivField->AddDataAttribute("attribute-type", $oAttDef->GetType());
$oDivField->AddDataAttribute("attribute-label", $sAttMetaDataLabel);
$oDivField->AddDataAttribute("attribute-flag-hidden", false);
$oDivField->AddDataAttribute("attribute-flag-read-only", false);
@@ -4660,7 +4810,7 @@ EOF
// Not in preview mode, do the update for real
$sTransactionId = utils::ReadPostedParam('transaction_id', '', 'transaction_id');
if (!utils::IsTransactionValid($sTransactionId, false)) {
throw new Exception(Dict::S('UI:Error:InvalidToken'));
throw new Exception(Dict::S('UI:Error:ObjectAlreadyUpdated'));
}
utils::RemoveTransaction($sTransactionId);
}
@@ -5218,6 +5368,7 @@ JS
/**
* @param array $aChanges
* @param bool $bIsNew
* @param string|null $sStimulusBeingApplied
*
* @return void
* @throws \ArchivedObjectException
@@ -5231,6 +5382,20 @@ JS
$this->FireEvent(EVENT_DB_AFTER_WRITE, ['is_new' => $bIsNew, 'changes' => $aChanges, 'stimulus_applied' => $sStimulusBeingApplied, 'cmdb_change' => self::GetCurrentChange()]);
}
//////////////
/// READ
///
/**
* @return void
* @throws \CoreException
* @since 3.3.0
*/
final public function FireEventReadDetails(string $sExportType): void
{
$this->FireEvent(EVENT_DATA_EXPORT, ['export_type' => $sExportType]);
}
//////////////
/// DELETE
///

View File

@@ -501,7 +501,7 @@ EOF
*/
public function Render($oPage, $bEditMode = false, $aExtraParams = [], $bCanEdit = true)
{
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? $this->GetId(), $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? null, $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
/** @var \DashboardLayoutMultiCol $oLayout */
$oLayout = new $this->sLayoutClass();
@@ -586,7 +586,7 @@ JS
/** @var \Dashlet $oDashlet */
foreach ($aCell as $oDashlet) {
if ($oDashlet->IsVisible()) {
$oPage->add('<div class="dashlet_properties" id="dashlet_properties_'.$oDashlet->GetID().'" data-role="ibo-dashlet-properties" data-dashlet-class="'.$oDashlet->GetDashletType().'" style="display:none">');
$oPage->add('<div class="dashlet_properties" id="dashlet_properties_'.$oDashlet->GetID().'" style="display:none">');
$oForm = $oDashlet->GetForm();
$this->SetFormParams($oForm, $aExtraParams);
$oForm->RenderAsPropertySheet($oPage, false, '.itop-dashboard');
@@ -1203,7 +1203,7 @@ EOF
}
$aRenderParams['dashboard_div_id'] = $aExtraParams['dashboard_div_id'];
$sJSExtraParams = json_encode($aExtraParams);
$oPage->add('<div id="dashboard_editor" class="ibo-dashboard-editor" data-role="ibo-dashboard-editor" data-dashboard-id="'.$this->sId.'">');
$oPage->add('<div id="dashboard_editor" class="ibo-dashboard-editor" data-role="ibo-dashboard-editor">');
$oPage->add('<div class="ui-layout-center">');
$this->SetCustomFlag(true);
$this->Render($oPage, true, $aRenderParams);

View File

@@ -25,20 +25,6 @@ use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
use Combodo\iTop\Application\UI\Base\iUIBlock;
use Combodo\iTop\Application\UI\Base\UIBlock;
use Combodo\iTop\Application\WebPage\WebPage;
use Combodo\iTop\Core\AttributeDefinition\AttributeDateTime;
use Combodo\iTop\Core\AttributeDefinition\AttributeDecimal;
use Combodo\iTop\Core\AttributeDefinition\AttributeDuration;
use Combodo\iTop\Core\AttributeDefinition\AttributeEncryptedString;
use Combodo\iTop\Core\AttributeDefinition\AttributeEnum;
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalField;
use Combodo\iTop\Core\AttributeDefinition\AttributeFriendlyName;
use Combodo\iTop\Core\AttributeDefinition\AttributeInteger;
use Combodo\iTop\Core\AttributeDefinition\AttributeLinkedSet;
use Combodo\iTop\Core\AttributeDefinition\AttributeOneWayPassword;
use Combodo\iTop\Core\AttributeDefinition\AttributePassword;
use Combodo\iTop\Core\AttributeDefinition\AttributePercentage;
use Combodo\iTop\Core\AttributeDefinition\AttributeSubItem;
use Combodo\iTop\Core\AttributeDefinition\iAttributeNoGroupBy;
require_once(APPROOT.'application/forms.class.inc.php');
@@ -454,7 +440,7 @@ EOF
// For external fields, find the real type of the target
$sExtFieldAttCode = $sAttCode;
$sTargetClass = $sClass;
while (is_a($sAttType, AttributeExternalField::class, true)) {
while (is_a($sAttType, 'AttributeExternalField', true)) {
$sExtKeyAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'extkey_attcode');
$sTargetAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'target_attcode');
$sTargetClass = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtKeyAttCode, 'targetclass');
@@ -464,13 +450,13 @@ EOF
}
$aForbidenAttType = [
AttributeLinkedSet::class,
AttributeFriendlyName::class,
'AttributeLinkedSet',
'AttributeFriendlyName',
iAttributeNoGroupBy::class, //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether
AttributeOneWayPassword::class,
AttributeEncryptedString::class,
AttributePassword::class,
'iAttributeNoGroupBy', //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether
'AttributeOneWayPassword',
'AttributeEncryptedString',
'AttributePassword',
];
foreach ($aForbidenAttType as $sForbidenAttType) {
if (is_a($sAttType, $sForbidenAttType, true)) {
@@ -482,7 +468,7 @@ EOF
if (!in_array($sLabel, $aGroupBy)) {
$aGroupBy[$sAttCode] = $sLabel;
if (is_a($sAttType, AttributeDateTime::class, true)) {
if (is_a($sAttType, 'AttributeDateTime', true)) {
$aGroupBy[$sAttCode.':hour'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Hour', $sLabel);
$aGroupBy[$sAttCode.':month'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Month', $sLabel);
$aGroupBy[$sAttCode.':day_of_week'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek', $sLabel);
@@ -1116,7 +1102,7 @@ abstract class DashletGroupBy extends Dashlet
$aAttributeTypes = $this->oModelReflection->ListAttributes($this->sClass);
if (isset($aAttributeTypes[$this->sGroupByAttCode])) {
$sAttributeType = $aAttributeTypes[$this->sGroupByAttCode];
if (is_subclass_of($sAttributeType, AttributeDateTime::class) || $sAttributeType == AttributeDateTime::class) {
if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') {
$this->sOrderDirection = 'asc';
} else {
$this->sOrderDirection = 'desc';
@@ -1272,7 +1258,7 @@ abstract class DashletGroupBy extends Dashlet
if ($this->oModelReflection->IsValidAttCode($sClass, $this->sGroupByAttCode)) {
$aAttributeTypes = $this->oModelReflection->ListAttributes($sClass);
$sAttributeType = $aAttributeTypes[$this->sGroupByAttCode];
if (is_subclass_of($sAttributeType, AttributeDateTime::class) || $sAttributeType == AttributeDateTime::class) {
if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') {
// Note: an alternative to this somewhat hardcoded way of doing things would be to implement...
//$oExpr = Expression::FromOQL($this->sGroupByExpr);
//$aTranslationData = array($oQuery->GetClassAlias() => array($this->sGroupByAttCode => new ScalarExpression(date('Y-m-d H:i:s', $iTime))));
@@ -1303,7 +1289,7 @@ abstract class DashletGroupBy extends Dashlet
foreach ($aValues as $sValue) {
$aDisplayValues[] = ['label' => $sValue, 'value' => (int)rand(1, 15)];
}
} elseif (is_subclass_of($sAttributeType, AttributeEnum::class) || $sAttributeType == AttributeEnum::class) {
} elseif (is_subclass_of($sAttributeType, 'AttributeEnum') || $sAttributeType == 'AttributeEnum') {
$aAllowed = $this->oModelReflection->GetAllowedValues_att($sClass, $this->sGroupByAttCode);
if ($aAllowed) { // null for non enums
foreach ($aAllowed as $sValue => $sValueLabel) {
@@ -1472,11 +1458,11 @@ abstract class DashletGroupBy extends Dashlet
}
foreach ($this->oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) {
switch ($sAttType) {
case AttributeDecimal::class:
case AttributeDuration::class:
case AttributeInteger::class:
case AttributePercentage::class:
case AttributeSubItem::class: // TODO: Known limitation: no unit displayed (values in sec)
case 'AttributeDecimal':
case 'AttributeDuration':
case 'AttributeInteger':
case 'AttributePercentage':
case 'AttributeSubItem': // TODO: Known limitation: no unit displayed (values in sec)
$sLabel = $this->oModelReflection->GetLabel($sClass, $sAttCode);
$aFunctionAttributes[$sAttCode] = $sLabel;
break;

View File

@@ -519,6 +519,31 @@ Call $this->AddInitialAttributeFlags($sAttCode, $iFlags) for all the initial att
</event_datum>
</event_data>
</event>
<event id="EVENT_DATA_EXPORT" _delta="define">
<name>Object details read from outside iTop</name>
<description><![CDATA[An object details has been read during an export]]></description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="object">
<description>The object unarchived</description>
<type>DBObject</type>
</event_datum>
<event_datum id="attributes">
<description>Attribute codes exposed (empty means potentially all attributes)</description>
<type>array</type>
</event_datum>
<event_datum id="export_type">
<description>Type of export</description>
<type>string</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_DOWNLOAD_DOCUMENT" _delta="define">
<name>Document downloaded</name>
<description><![CDATA[A document has been downloaded from the GUI]]></description>
@@ -563,17 +588,6 @@ Call $this->AddInitialAttributeFlags($sAttCode, $iFlags) for all the initial att
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_LOCATOR_INITIALIZED" _delta="define">
<name>Service Locator Initialized</name>
<internal>true</internal>
<description><![CDATA[Inform the listeners that Service Locator has been (re)initialized]]></description>
<event_data>
<event_datum id="file_name">
<description>The configuration file used to initialize Service Locator</description>
<type>string</type>
</event_datum>
</event_data>
</event>
</events>
<meta>
<classes>

View File

@@ -1707,7 +1707,7 @@ JS
$oBlock->bAdvancedMode = utils::ReadParam('advanced', false);
$oBlock->sCsvFile = strtolower($this->m_oFilter->GetClass()).'.csv';
$oBlock->sDownloadLink = utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php?expression='.urlencode($this->m_oFilter->ToOQL(true)).'&format=csv&filename='.urlencode($oBlock->sCsvFile);
$oBlock->sDownloadLink = utils::GetAbsoluteUrlAppRoot().'webservices/export.php?expression='.urlencode($this->m_oFilter->ToOQL(true)).'&format=csv&filename='.urlencode($oBlock->sCsvFile);
$oBlock->sLinkToToggle = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=search'.$oAppContext->GetForLink(true).'&filter='.rawurlencode($this->m_oFilter->serialize()).'&format=csv';
// Pass the parameters via POST, since expression may be very long
$aParamsToPost = [
@@ -1724,7 +1724,7 @@ JS
$oBlock->sLinkToToggle = $oBlock->sLinkToToggle.'&advanced=1';
$oBlock->sChecked = '';
}
$oBlock->sAjaxLink = utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php';
$oBlock->sAjaxLink = utils::GetAbsoluteUrlAppRoot().'webservices/export.php';
$oBlock->sCharsetNotice = false;
$oBlock->sJsonParams = json_encode($aParamsToPost);
@@ -2292,17 +2292,6 @@ class MenuBlock extends DisplayBlock
}
// ResourceId should not be sanitized
$oActionButton->AddDataAttribute('resource-id', $sActionId);
if (isset($aAction['data_attributes']) && is_array($aAction['data_attributes'])) {
foreach ($aAction['data_attributes'] as $sName => $sValue) {
$sDataAttributeName = $sName;
$oActionButton->AddDataAttribute($sDataAttributeName, (string) $sValue);
}
}
if (isset($aAction['aria_attributes']) && is_array($aAction['aria_attributes'])) {
foreach ($aAction['aria_attributes'] as $sName => $sValue) {
$oActionButton->AddAriaAttribute((string) $sName, (string) $sValue);
}
}
$oActionButton->AddCSSClasses(['ibo-action-button', 'ibo-regular-action-button']);
if (empty($sLabel)) {
if (empty($aAction['tooltip'])) {

View File

@@ -220,19 +220,10 @@ class DesignerForm
$sValidationFields = '</td><td class="prop_icon prop_apply ibo-prop--apply" >'.$sValidation.'</td><td class="prop_icon prop_cancel ibo-prop--cancel"><span><div class="ibo-button ibo-is-alternative ibo-is-neutral" data-tooltip-content="'.Dict::Format('UI:DashboardEdit:Revert').'"><i class="fas fa-undo"></i></div></span></td>'
.$this->EndRow();
$aPropertyFieldData = [
'code' => $oField->GetCode(),
'type' => $oField->GetInputType(),
'label' => $aRow['label'],
'raw-value' => $oField->GetRawValue(),
];
if (is_null($aRow['label'])) {
$sReturn .= $this->StartRow($sFieldId, $aPropertyFieldData).
'<td class="prop_value" data-role="ibo-property-field-value" colspan="2">'.$aRow['value'];
$sReturn .= $this->StartRow($sFieldId).'<td class="prop_value" colspan="2">'.$aRow['value'];
} else {
$sReturn .= $this->StartRow($sFieldId, $aPropertyFieldData).
'<td class="prop_label" data-role="ibo-property-field-label">'.$aRow['label'].
'</td><td class="prop_value" data-role="ibo-property-field-value">'.$aRow['value'];
$sReturn .= $this->StartRow($sFieldId).'<td class="prop_label">'.$aRow['label'].'</td><td class="prop_value">'.$aRow['value'];
}
if (!($oField instanceof DesignerFormSelectorField) && !($oField instanceof DesignerMultipleSubFormField)) {
$sReturn .= $sValidationFields;
@@ -303,21 +294,12 @@ EOF
}
}
public function StartRow($sFieldId = null, $aPropertyFieldData = [])
public function StartRow($sFieldId = null)
{
$sPropertyFieldAttributes = '';
foreach ($aPropertyFieldData as $sAttribute => $value) {
if (is_array($value)) {
$value = json_encode($value);
} elseif (is_null($value)) {
$value = '';
}
$sPropertyFieldAttributes .= ' data-property-field-'.$sAttribute.'="'.utils::EscapeHtml((string)$value).'"';
}
if ($sFieldId != null) {
return '<tr id="row_'.$sFieldId.'" data-path="'.$this->GetHierarchyPath().'" data-selector="'.$this->GetHierarchyParent().'"'.$sPropertyFieldAttributes.'>';
return '<tr id="row_'.$sFieldId.'" data-path="'.$this->GetHierarchyPath().'" data-selector="'.$this->GetHierarchyParent().'">';
}
return '<tr data-path="'.$this->GetHierarchyPath().'" data-selector="'.$this->GetHierarchyParent().'"'.$sPropertyFieldAttributes.'>';
return '<tr data-path="'.$this->GetHierarchyPath().'" data-selector="'.$this->GetHierarchyParent().'">';
}
public function EndRow()
@@ -697,14 +679,6 @@ class DesignerFormField
return $this->sCode;
}
/**
* @return mixed
*/
public function GetRawValue()
{
return $this->defaultValue;
}
/**
* @param \DesignerForm $oForm
*/
@@ -1655,11 +1629,6 @@ class DesignerFormSelectorField extends DesignerFormField
return 'selector_property_field';
}
public function GetRawValue()
{
return $this->defaultRealValue;
}
public function AddSubForm($oSubForm, $sLabel, $sValue)
{
$this->aSubForms[] = ['form' => $oSubForm, 'label' => $sLabel, 'value' => $sValue];

View File

@@ -25,11 +25,11 @@
*/
use Combodo\iTop\Application\Branding;
use Combodo\iTop\Application\Helper\Session;
use Combodo\iTop\Application\WebPage\ErrorPage;
use Combodo\iTop\Application\WebPage\NiceWebPage;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Service\Session\Session;
/**
* Web page used for displaying the login form
@@ -369,7 +369,16 @@ class LoginWebPage extends NiceWebPage
public static function ResetSession()
{
// Unset all of the session variables.
Session::UnsetAll();
Session::Unset('auth_user');
Session::Unset('login_state');
Session::Unset('can_logoff');
Session::Unset('archive_mode');
Session::Unset('impersonate_user');
Session::Unset('PluginProperties');
Session::Unset('UrlMakerClass');
Session::Unset('itop_env');
Session::Unset('obj_messages');
Session::Unset('profile_list');
UserRights::_ResetSessionCache();
// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!

View File

@@ -115,7 +115,7 @@ class privUITransactionSession
// Strictly speaking, the two lines below should be grouped together
// by a critical section
// sem_acquire($rSemIdentified);
$id = static::GetUserPrefix().str_replace(['.', ' '], '', microtime()).'-'.bin2hex(random_bytes(24));
$id = static::GetUserPrefix().str_replace(['.', ' '], '', microtime());
Session::Set(['transactions', $id], true);
// sem_release($rSemIdentified);
@@ -236,7 +236,7 @@ class privUITransactionFile
self::CleanupOldTransactions();
$sTransactionIdFullPath = static::CreateUniqueTransactionFilePath(utils::GetDataPath().'transactions', static::GetUserPrefix());
$sTransactionIdFullPath = tempnam(utils::GetDataPath().'transactions', static::GetUserPrefix());
file_put_contents($sTransactionIdFullPath, $iCurrentUserId, LOCK_EX);
$sTransactionIdFileName = basename($sTransactionIdFullPath);
@@ -245,33 +245,6 @@ class privUITransactionFile
return $sTransactionIdFileName;
}
/**
* Create a transaction file path with a longer random suffix and create it atomically.
*
* @param string $sDir
* @param string $sPrefix
*
* @return string
*
* @throws CoreException
*/
private static function CreateUniqueTransactionFilePath(string $sDir, string $sPrefix): string
{
$iMaxAttempts = 10;
for ($i = 0; $i < $iMaxAttempts; $i++) {
$sFileName = $sPrefix.bin2hex(random_bytes(24)); // 48 random hex chars
$sFullPath = $sDir.'/'.$sFileName;
$hFile = @fopen($sFullPath, 'x');
if ($hFile !== false) {
fclose($hFile);
return $sFullPath;
}
}
throw new CoreException('Failed to allocate a unique transaction file name after multiple attempts.');
}
/**
* Check whether a transaction is valid or not and (optionally) remove the valid transaction from
* the session so that another call to IsTransactionValid for the same transaction id
@@ -308,18 +281,6 @@ class privUITransactionFile
return false;
}
$iTime = filemtime($sFilepath);
$iLifetime = (int) MetaModel::GetConfig()->Get('transactions_file_lifetime');
$iLimit = time() - $iLifetime;
if ($iTime < $iLimit) {
self::Info("IsTransactionValid: Transaction '$id' expired.");
if ($iLifetime < 3600) {
IssueLog::Warning("IsTransactionValid: Transaction '$id' expired after only $iLifetime seconds. Consider increasing the 'transactions_file_lifetime' configuration parameter.");
}
@unlink($sFilepath);
return false;
}
if ($bRemoveTransaction) {
$bResult = @unlink($sFilepath);
if (!$bResult) {
@@ -353,7 +314,7 @@ class privUITransactionFile
}
/**
* Cleanup old transactions which have been pending since more than the lifetime defined in the configuration parameter 'transactions_file_lifetime'.
* Cleanup old transactions which have been pending since more than 24 hours
* Use filemtime instead of filectime since filectime may be affected by operations on the directory (like changing the access rights)
*/
protected static function CleanupOldTransactions($sTransactionDir = null)
@@ -366,7 +327,7 @@ class privUITransactionFile
}
clearstatcache();
$iLimit = time() - (int) MetaModel::GetConfig()->Get('transactions_file_lifetime');
$iLimit = time() - 24 * 3600;
$sPattern = $sTransactionDir ? "$sTransactionDir/*" : utils::GetDataPath().'transactions/*';
$aTransactions = glob($sPattern);
foreach ($aTransactions as $sFileName) {

View File

@@ -1833,15 +1833,10 @@ SQL;
/**
* Some characters cause troubles with jQuery when used inside DOM IDs, so let's replace them by the safe _ (underscore)
* @param string $sId The ID to sanitize
* @param bool $bForSelector Whether to apply strict CSS/jQuery selector-safe sanitization
* @return string The sanitized ID
*/
public static function GetSafeId($sId, bool $bForSelector = false)
public static function GetSafeId($sId)
{
if ($bForSelector) {
return preg_replace('/[^a-zA-Z0-9_]/', '_', $sId);
}
return str_replace([':', '[', ']', '+', '-', ' '], '_', $sId);
}
@@ -1891,7 +1886,7 @@ SQL;
CURLOPT_HEADER => false, // don't return the headers in the output
CURLOPT_FOLLOWLOCATION => true, // follow redirects
CURLOPT_ENCODING => "", // handle all encodings
CURLOPT_USERAGENT => static::GetConfig()->Get('http.request.user_agent'), // who am i
CURLOPT_USERAGENT => "spider", // who am i
CURLOPT_AUTOREFERER => true, // set referer on redirect
CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect
CURLOPT_TIMEOUT => 120, // timeout on response
@@ -3027,6 +3022,7 @@ TXT
* Note: Only works for backoffice URLs for now
*
* @param string $sText Text containing the mentioned objects to be found
* @param string $sFormat {@uses static::ENUM_TEXT_FORMAT_HTML, ...}
*
* @return array Array of object classes / IDs for the ones found in $sText
*
@@ -3041,45 +3037,13 @@ TXT
public static function GetMentionedObjectsFromText(string $sText): array
{
$aMentionedObjects = [];
$aMentionAllowedClasses = MetaModel::GetConfig()->Get('mentions.allowed_classes');
$oDom = new \DOMDocument();
$bPreviousUseInternalErrors = libxml_use_internal_errors(true); // to keep processing even in case of "invalid" HTML, cf. testGetMentionedObjectsFromText
$aMentionMatches = [];
$sText = html_entity_decode($sText);
try {
$oDom->loadHTML('<?xml encoding="UTF-8">'.$sText);
} finally {
libxml_clear_errors();
libxml_use_internal_errors($bPreviousUseInternalErrors);
}
$oXpath = new \DOMXPath($oDom);
$oNodes = $oXpath->query('//a[@data-object-class and @data-object-key]');
foreach ($oNodes as $oObjNode) {
$sMatchedClass = $oObjNode->getAttribute('data-object-class');
$sMatchedId = $oObjNode->getAttribute('data-object-key');
$sMatchedName = trim($oObjNode->textContent);
// Ensure that what we found is actually configured as a mention
$sMentionPrefix = array_search($sMatchedClass, $aMentionAllowedClasses);
// - No direct configuration for the matched class, let's look for a configuration on parent classes
if (false === $sMentionPrefix) {
$bHasMentionConfigurationForParentClass = false;
foreach (MetaModel::EnumParentClasses($sMatchedClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false) as $aMatchedParentClass) {
$sMentionPrefix = array_search($aMatchedParentClass, $aMentionAllowedClasses);
if (false !== $sMentionPrefix) {
$bHasMentionConfigurationForParentClass = true;
break;
}
}
if (false === $bHasMentionConfigurationForParentClass) {
continue;
}
}
// - Test if the name starts with $sMentionPrefix (e.g. '@' for 'Contact' class)
if (false === str_starts_with($sMatchedName, $sMentionPrefix)) {
continue;
}
preg_match_all('/<a\s*([^>]*)data-object-class="([^"]*)"\s.*data-object-key="([^"]*)"/Ui', $sText, $aMentionMatches);
foreach ($aMentionMatches[0] as $iMatchIdx => $sCompleteMatch) {
$sMatchedClass = $aMentionMatches[2][$iMatchIdx];
$sMatchedId = $aMentionMatches[3][$iMatchIdx];
// Prepare array for matched class if not already present
if (!array_key_exists($sMatchedClass, $aMentionedObjects)) {
@@ -3254,48 +3218,4 @@ TXT
return (int)$sLimit;
}
}
/**
* @since 3.3.0 N°9875
* Open archive and raise appropriate exception.
* Warning: do not forget to close archive afterwhile
* @param string $sArchiveFilePath
* @param int|null $flags
* @return ZipArchive
* @throws \Exception
*/
public static function ZipArchiveOpen(string $sArchiveFilePath, int|null $flags = null): ZipArchive
{
$oZip = new ZipArchive();
if (is_null($flags)) {
$code = $oZip->open($sArchiveFilePath);
} else {
$code = $oZip->open($sArchiveFilePath, $flags);
}
if (true !== $code) {
//ZipArchive::ZIP_ER_NOZIP : 19
if ($code === 19) {
throw new \Exception(sprintf('Cannot open zip file due to inconsistent or empty content'));
}
throw new \Exception(sprintf('Cannot open zip file due to error code %s', $code));
}
return $oZip;
}
/**
* @since 3.3.0 N°9875
* @param string $sDirectory
* @param string $sPrefix
* @return ZipArchive
* @throws \Exception
*/
public static function ZipArchiveCreateWithTempNam(string $sDirectory, string $sPrefix): ZipArchive
{
$sTempnam = tempnam($sDirectory, $sPrefix);
unlink($sTempnam);
$sArchiveName = $sTempnam.'.zip';
return self::ZipArchiveOpen($sArchiveName, ZipArchive::CREATE);
}
}

View File

@@ -607,14 +607,6 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'attribute.max_actions_items' => [
'type' => 'integer',
'description' => 'Maximum number of actions to display before it\'s compacted into a single action (0 means no limit)',
'default' => 2,
'value' => 2,
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'email_asynchronous' => [
'type' => 'bool',
'description' => 'If set, the emails are sent off line, which requires cron.php to be activated. Exception: some features like the email test utility will force the serialized mode',
@@ -1296,14 +1288,6 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'transactions_file_lifetime' => [
'type' => 'integer',
'description' => 'Value in seconds for the lifetime of a transaction file (after this duration, the transaction will be invalid and the garbage collector could delete it). This only works if the "transaction_storage" parameter is set to "File".',
'default' => 3600,
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'log_transactions' => [
'type' => 'bool',
'description' => 'Whether or not to enable the debug log for the transactions.',

View File

@@ -334,12 +334,14 @@ EOF
while ($aRow = $oSet->FetchAssoc()) {
set_time_limit(intval($iLoopTimeLimit));
$aData = [];
foreach ($this->aStatusInfo['fields'] as $iCol => $aFieldSpec) {
$aExportedObjects = [];
foreach ($this->aStatusInfo['fields'] as $aFieldSpec) {
$sAlias = $aFieldSpec['sAlias'];
$sAttCode = $aFieldSpec['sAttCode'];
$sField = '';
$oObj = $aRow[$sAlias];
$aExportedObjects[] = $oObj;
if ($oObj != null) {
switch ($sAttCode) {
case 'id':
@@ -366,7 +368,13 @@ EOF
}
$sData .= implode($this->aStatusInfo['separator'], $aData)."\n";
$iCount++;
$aExportedObjects = array_unique($aExportedObjects);
foreach ($aExportedObjects as $oExportedObject) {
$oExportedObject->FireEventReadDetails(get_class($this));
}
}
// Restore original date & time formats
AttributeDateTime::SetFormat($oPrevDateTimeFormat);
AttributeDate::SetFormat($oPrevDateFormat);

View File

@@ -730,8 +730,13 @@ abstract class DBObject implements iDisplay
public function SetTrim($sAttCode, $sValue)
{
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$this->Set($sAttCode, $oAttDef->TrimValue($sValue));
$iMaxSize = $oAttDef->GetMaxSize();
$sLength = mb_strlen($sValue);
if ($iMaxSize && ($sLength > $iMaxSize)) {
$sMessage = " -truncated ($sLength chars)";
$sValue = mb_substr($sValue, 0, $iMaxSize - mb_strlen($sMessage)).$sMessage;
}
$this->Set($sAttCode, $sValue);
}
/**
@@ -2033,7 +2038,7 @@ abstract class DBObject implements iDisplay
}
}
if (!is_null($iMaxSize = $oAtt->GetMaxSize())) {
$iLen = $oAtt->GetSize($toCheck);
$iLen = mb_strlen($toCheck);
if ($iLen > $iMaxSize) {
return "String too long (found $iLen, limited to $iMaxSize)";
}
@@ -6242,7 +6247,9 @@ abstract class DBObject implements iDisplay
}
/**
* @param array $aChanges
* @param bool $bIsNew
* @param string|null $sStimulusBeingApplied
*
* @return void
* @since 3.1.0
@@ -6251,6 +6258,18 @@ abstract class DBObject implements iDisplay
{
}
//////////////
/// READ
///
/**
* @return void
* @since 3.3.0
*/
public function FireEventReadDetails(string $sExportType): void
{
}
//////////////
/// DELETE
///

View File

@@ -288,11 +288,13 @@ EOF
while ($aRow = $oSet->FetchAssoc()) {
set_time_limit(intval($iLoopTimeLimit));
$aData = [];
$aExportedObjects = [];
foreach ($this->aStatusInfo['fields'] as $iCol => $aFieldSpec) {
$sAlias = $aFieldSpec['sAlias'];
$sAttCode = $aFieldSpec['sAttCode'];
$oObj = $aRow[$sAlias];
$aExportedObjects[] = $oObj;
$sField = '';
if ($oObj) {
$sField = $this->GetValue($oObj, $sAttCode);
@@ -301,6 +303,11 @@ EOF
}
fwrite($hFile, json_encode($aData)."\n");
$iCount++;
$aExportedObjects = array_unique($aExportedObjects);
foreach ($aExportedObjects as $oExportedObject) {
$oExportedObject->FireEventReadDetails(get_class($this));
}
}
set_time_limit(intval($iPreviousTimeLimit));
$this->aStatusInfo['position'] += $this->iChunkSize;

View File

@@ -139,6 +139,7 @@ class HTMLBulkExport extends TabularBulkExport
} else {
$sData .= "<tr>";
}
$aExportedObjects = [];
foreach ($this->aStatusInfo['fields'] as $iCol => $aFieldSpec) {
$sAlias = $aFieldSpec['sAlias'];
$sAttCode = $aFieldSpec['sAttCode'];
@@ -147,12 +148,18 @@ class HTMLBulkExport extends TabularBulkExport
$sField = '';
if ($oObj) {
$sField = $this->GetValue($oObj, $sAttCode);
$aExportedObjects[] = $oObj;
}
$sValue = ($sField === '') ? '&nbsp;' : $sField;
$sData .= "<td>$sValue</td>";
}
$sData .= "</tr>";
$iCount++;
$aExportedObjects = array_unique($aExportedObjects);
foreach ($aExportedObjects as $oExportedObject) {
$oExportedObject->FireEventReadDetails(get_class($this));
}
}
set_time_limit(intval($iPreviousTimeLimit));
$this->aStatusInfo['position'] += $this->iChunkSize;

View File

@@ -20,7 +20,6 @@
use Combodo\iTop\Application\EventRegister\ApplicationEvents;
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
use Combodo\iTop\Service\DependencyInjection\ServiceLocator;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
@@ -131,8 +130,6 @@ abstract class MetaModel
private static $m_aClassToFile = [];
/** @var string */
protected static $m_sEnvironment = ITOP_DEFAULT_ENV;
/** @var \Combodo\iTop\Service\DependencyInjection\ServiceLocator */
private static ?ServiceLocator $oServiceLocator = null;
/**
* Objects currently created/updated.
@@ -1645,10 +1642,10 @@ abstract class MetaModel
if ($sFilterCode == 'id') {
return true;
}
if (array_key_exists($sClass, self::$m_aMagicFields) && in_array($sFilterCode, self::$m_aMagicFields[$sClass])) {
if (array_key_exists($sClass, self::$m_aMagicFields) && array_key_exists($sFilterCode, self::$m_aMagicFields[$sClass])) {
return true;
}
if (array_key_exists($sClass, self::$m_aFilterForbiddenAttributes) && in_array($sFilterCode, self::$m_aFilterForbiddenAttributes[$sClass])) {
if (array_key_exists($sClass, self::$m_aFilterForbiddenAttributes) && array_key_exists($sFilterCode, self::$m_aFilterForbiddenAttributes[$sClass])) {
return false;
}
return self::IsValidAttCode($sClass, $sFilterCode);
@@ -2966,9 +2963,8 @@ abstract class MetaModel
self::$m_aMagicFields[] = $sClass;
}
self::$m_aMagicFields[$sClass][] = $sCode;
self::$m_aAttribOrigins[$sClass][$sCode] = self::$m_aAttribOrigins[$sClass][$sAttCode];
}
if (!$oAttDef->IsValidFilterAttribute()) {
if (!$oAttDef->IsSearchable()) {
if (!array_key_exists($sClass, self::$m_aFilterForbiddenAttributes)) {
self::$m_aFilterForbiddenAttributes[] = $sClass;
}
@@ -5781,7 +5777,6 @@ abstract class MetaModel
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
EventService::InitService();
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
self::$oServiceLocator = new ServiceLocator(self::GetConfig());
return;
}
}
@@ -5797,8 +5792,6 @@ abstract class MetaModel
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
EventService::InitService();
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
self::$oServiceLocator = new ServiceLocator();
self::$oServiceLocator->InitFromConfig(self::GetConfig());
}
/**
@@ -6872,24 +6865,10 @@ abstract class MetaModel
require_once(APPROOT.'/core/dict.class.inc.php');
Dict::ResetCache($sAppIdentity);
$iApcDeleteFailureCount = 0;
if (function_exists('apc_delete')) {
foreach (self::GetCacheEntries($sEnvironmentId) as $sKey => $aAPCInfo) {
$sAPCKey = $aAPCInfo['info'];
if (!apc_delete($sAPCKey)) {
$iApcDeleteFailureCount++;
}
}
}
if ($iApcDeleteFailureCount > 0) {
SetupLog::Warning("apc_delete failed for $iApcDeleteFailureCount entries for env $sEnvironment");
}
if (function_exists('apc_clear_cache')) {
// old style APC
if (!apc_clear_cache()) {
SetupLog::Warning("apc_clear_cache failed for env $sEnvironment");
apc_delete($sAPCKey);
}
}
@@ -6899,9 +6878,7 @@ abstract class MetaModel
// Reset the opcache since otherwise the PHP "model" files may still be cached !!
if (function_exists('opcache_reset')) {
// Zend opcode cache
if (!opcache_reset()) {
SetupLog::Warning("opcache_reset failed for env $sEnvironment");
}
opcache_reset();
}
require_once(APPROOT.'setup/setuputils.class.inc.php');
@@ -7091,11 +7068,6 @@ abstract class MetaModel
}
}
}
public static function GetServiceLocator(): ?ServiceLocator
{
return self::$oServiceLocator;
}
}
// Standard attribute lists

View File

@@ -60,13 +60,15 @@ class OQLActualClassTreeResolver
$aTranslateFields = [];
foreach ($aExpectedAttributes as $sAttCode => $oExpression) {
// 'id' is managed later
if ($sAttCode === 'id') {
if ($sAttCode == 'id') {
continue;
}
// Attributes can be stored in attributes list or for magic ones into filter codes list.
$sOriginClass = null;
if (MetaModel::IsValidAttCode($sClass, $sAttCode) || MetaModel::IsValidFilterCode($sClass, $sAttCode)) {
if (MetaModel::IsValidAttCode($sClass, $sAttCode)) {
$sOriginClass = MetaModel::GetAttributeOrigin($sClass, $sAttCode);
} elseif ($sAttCode == 'id') {
$sOriginClass = $sClass;
} else {
continue;
}

View File

@@ -625,6 +625,7 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
}
while ($oObject = $oObjectSet->Fetch()) {
$oObject->FireEventReadDetails(get_class($this));
$oResult->AddObject(0, '', $oObject, $aShowFields, RestUtils::HasRequestedExtendedOutput($sShowFields));
}
$oResult->message = "Found: ".$oObjectSet->Count();
@@ -699,6 +700,7 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
if ($oElement instanceof RelationObjectNode) {
$oObject = $oElement->GetProperty('object');
if ($oObject) {
$oObject->FireEventReadDetails(get_class($this));
if ($bEnableRedundancy && $sDirection == 'down') {
// Add only the "reached" objects
if ($oElement->GetProperty('is_reached')) {

View File

@@ -233,7 +233,6 @@ EOF
public function GetNextChunk(&$aStatus)
{
$sRetCode = 'run';
$iPercentage = 0;
$oSet = new DBObjectSet($this->oSearch);
$oSet->SetLimit($this->iChunkSize, $this->aStatusInfo['position']);
@@ -254,7 +253,8 @@ EOF
set_time_limit(intval($iLoopTimeLimit));
$sData .= "<tr>";
foreach ($this->aStatusInfo['fields'] as $iCol => $aFieldSpec) {
$aExportedObjects = [];
foreach ($this->aStatusInfo['fields'] as $aFieldSpec) {
$sAlias = $aFieldSpec['sAlias'];
$sAttCode = $aFieldSpec['sAttCode'];
@@ -265,7 +265,7 @@ EOF
$sData .= "<td x:str></td>";
continue;
}
$aExportedObjects[] = $oObj;
switch ($sAttCode) {
case 'id':
$sField = $oObj->GetKey();
@@ -322,6 +322,11 @@ EOF
}
$sData .= "</tr>";
$iCount++;
$aExportedObjects = array_unique($aExportedObjects);
foreach ($aExportedObjects as $oExportedObject) {
$oExportedObject->FireEventReadDetails(get_class($this));
}
}
set_time_limit(intval($iPreviousTimeLimit));
$this->aStatusInfo['position'] += $this->iChunkSize;

View File

@@ -8,13 +8,6 @@
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
use Combodo\iTop\Application\WebPage\WebPage;
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalField;
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalKey;
use Combodo\iTop\Core\AttributeDefinition\AttributeFriendlyName;
use Combodo\iTop\Core\AttributeDefinition\AttributeHierarchicalKey;
use Combodo\iTop\Core\AttributeDefinition\AttributeLinkedSet;
use Combodo\iTop\Core\AttributeDefinition\AttributeStopWatch;
use Combodo\iTop\Core\AttributeDefinition\AttributeSubItem;
/**
* Bulk export: Tabular export: abstract base class for all "tabular" exports.
@@ -98,8 +91,8 @@ abstract class TabularBulkExport extends BulkExport
{
$aResult = [];
switch (get_class($oAttDef)) {
case AttributeExternalKey::class:
case AttributeHierarchicalKey::class:
case 'AttributeExternalKey':
case 'AttributeHierarchicalKey':
$bAddFriendlyName = true;
$oKeyAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
@@ -149,7 +142,7 @@ abstract class TabularBulkExport extends BulkExport
}
break;
case AttributeStopWatch::class:
case 'AttributeStopWatch':
foreach (MetaModel::ListAttributeDefs($sClass) as $sSubAttCode => $oSubAttDef) {
if ($oSubAttDef instanceof AttributeSubItem) {
if ($oSubAttDef->GetParentAttCode() == $sAttCode) {
@@ -325,7 +318,7 @@ EOF
$sQueryId = utils::ReadParam('query', null, true);
$sFields = utils::ReadParam('fields', null, true, 'raw_data');
if ((($sFields === null) || ($sFields === '')) && ($sQueryId === null)) {
$sFields = $this->GetFieldsFromQuery();
throw new BulkExportMissingParameterException('fields');
} else {
if (($sQueryId !== null) && ($sQueryId !== null)) {
$oSearch = DBObjectSearch::FromOQL('SELECT QueryOQL WHERE id = :query_id', ['query_id' => $sQueryId]);
@@ -336,11 +329,11 @@ EOF
// No 'fields' parameter supplied, take the fields from the query phrasebook definition
$sFields = trim($oQuery->Get('fields'));
if ($sFields === '') {
$sFields = $this->GetFieldsFromQuery();
throw new BulkExportMissingParameterException('fields');
}
}
} else {
throw new BulkExportException('Invalid value for the parameter: query. There is no Query Phrasebook with id = '.$sQueryId, Dict::Format('Core:BulkExport:InvalidParameter_Query', $sQueryId));
throw BulkExportException('Invalid value for the parameter: query. There is no Query Phrasebook with id = '.$sQueryId, Dict::Format('Core:BulkExport:InvalidParameter_Query', $sQueryId));
}
}
}
@@ -348,39 +341,6 @@ EOF
$this->SetFields($sFields);
}
public function GetFieldsFromQuery(): string
{
if (is_null($this->oSearch)) {
throw new BulkExportMissingParameterException("fields");
}
$aSelectedClasses = $this->oSearch->GetSelectedClasses();
$aAuthorizedClasses = [];
$aFields = [];
foreach ($aSelectedClasses as $sAlias => $sClassName) {
if (UserRights::IsActionAllowed($sClassName, UR_ACTION_BULK_READ) == UR_ALLOWED_YES) {
$aAuthorizedClasses[$sAlias] = $sClassName;
}
}
foreach ($aAuthorizedClasses as $sAlias => $sClassName) {
foreach (MetaModel::ListAttributeDefs($sClassName) as $oAttribute) {
$sAttCode = $oAttribute->GetCode();
if (! $this->IsExportableField($sClassName, $sAttCode, $oAttribute)) {
continue;
}
$sCurrentAlias = utils::IsNullOrEmptyString($sAlias) ? "" : "$sAlias.";
$aFields[] = "{$sCurrentAlias}{$sAttCode}";
}
}
if (count($aFields) === 0) {
IssueLog::Error("User is not allowed to see any field for exported OQL");
throw new BulkExportMissingParameterException("fields");
}
return implode(',', $aFields);
}
public function SetFields($sFields)
{
// Interpret (and check) the list of fields

View File

@@ -135,6 +135,11 @@ abstract class Trigger extends cmdbAbstractObject
if ($oAction->IsActive()) {
$oKPI = new ExecutionKPI();
$aContextArgs['action->object()'] = $oAction;
if (array_key_exists('this->object()', $aContextArgs)) {
/** @var \DBObject $oObject */
$oObject = $aContextArgs['this->object()'];
$oObject->FireEventReadDetails(get_class($oAction));
}
$oAction->DoExecute($this, $aContextArgs);
$oKPI->ComputeStatsForExtension($oAction, 'DoExecute');
}

View File

@@ -144,6 +144,7 @@ class XMLBulkExport extends BulkExport
if (count($aAuthorizedClasses) > 1) {
$sData .= "<Row>\n";
}
$aExportedObjects = [];
foreach ($aAuthorizedClasses as $sAlias => $sClassName) {
$oObj = $aObjects[$sAlias];
if (is_null($oObj)) {
@@ -151,6 +152,7 @@ class XMLBulkExport extends BulkExport
} else {
$sClassName = get_class($oObj);
$sData .= "<$sClassName alias=\"$sAlias\" id=\"".$oObj->GetKey()."\">\n";
$aExportedObjects[] = $oObj;
}
foreach ($aClass2Attributes[$sAlias] as $sAttCode => $oAttDef) {
if (is_null($oObj)) {
@@ -166,6 +168,11 @@ class XMLBulkExport extends BulkExport
$sData .= "</Row>\n";
}
$iCount++;
$aExportedObjects = array_unique($aExportedObjects);
foreach ($aExportedObjects as $oExportedObject) {
$oExportedObject->FireEventReadDetails(get_class($this));
}
}
set_time_limit(intval($iPreviousTimeLimit));

View File

@@ -42,7 +42,7 @@ css/backoffice/
|– vendors/ # Third-party libs, should be either:
| # - Overload of the lib SCSS variables (BEST way, but possible only if the lib exposes them. e.g. Bulma)
| # - Overload of the lib necessary CSS classes only (not great as it duplicates some rules in the browser, which add weight and computation. e.g. dataTables)
| # - Duplicate the lib CSS completely to insert SCSS variables (not great as it will be outdated when updating the lib itself. e.g. jQuery UI)
| # - Duplicate the lib CSS completly to insert SCSS variables (not great as it will be outdated when updating the lib itself. e.g. jQuery UI)
| |– _bulma-variables-overload.scss # Bulma CSS framework
| |– _jquery-ui.scss # jQuery UI
| ... # Etc…

View File

@@ -4,7 +4,7 @@
*/
$ibo-user-rights--padding-x: $ibo-spacing-400 !default;
$ibo-user-rights--padding-y: $ibo-spacing-100 !default;
$ibo-user-rights--padding-y: $ibo-spacing-200 !default;
$ibo-user-rights--border-radius: $ibo-border-radius-400 !default;
$ibo-user-rights--is-success--background-color: $ibo-color-success-100 !default;
@@ -18,8 +18,6 @@ $ibo-user-rights--is-failure--border-color: $ibo-color-danger-500 !default;
$ibo-user-rights--is-failure--border: 1px solid $ibo-user-rights--is-failure--border-color !default;
.ibo-user-rights {
display: inline-flex;
align-items: baseline;
padding: $ibo-user-rights--padding-y $ibo-user-rights--padding-x;
border-radius: $ibo-user-rights--border-radius;
&.ibo-is-success {

View File

@@ -5,8 +5,6 @@
@import "alert";
@import "button";
@import "button-bar";
@import "button-separator";
@import "button-group";
@import "breadcrumbs";
@import "quick-create";

View File

@@ -1,43 +0,0 @@
/*
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
$ibo-button-bar--gap: 2px !default;
$ibo-button-bar--track--gap: 6px !default;
/* button-bar.css */
ibo-button-bar{
display: flex;
flex-wrap: nowrap;
align-items: center;
position: relative;
gap: $ibo-button-bar--gap;
overflow: hidden;
}
/* Keep content visible even if JS initialization is delayed. */
ibo-button-bar .ibo-button-bar--track{
display: flex;
flex: 1 1 auto;
flex-wrap: nowrap;
overflow: hidden;
white-space: nowrap;
gap: $ibo-button-bar--track--gap;
align-items: center;
}
ibo-button-bar .ibo-button-bar--item{
flex: 0 0 auto;
white-space: nowrap;
}
ibo-button-bar .ibo-button-bar--item[hidden]{
display: none !important;
}
ibo-button-bar .ibo-button-bar--extra{
flex: 0 0 auto;
position: relative;
}

View File

@@ -1,20 +0,0 @@
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
$ibo-button-separator--width: 1px !default;
$ibo-button-separator--height: 16px !default;
$ibo-button-separator--margin-x: 0.75rem !default;
$ibo-button-separator--margin-y: $ibo-spacing-0 !default;
$ibo-button-separator--border-size: 1px !default;
$ibo-button-separator--border-color: $ibo-color-grey-500 !default;
.ibo-button-separator {
display: inline-flex;
border-right: $ibo-button-separator--border-size solid $ibo-button-separator--border-color;
width: $ibo-button-separator--width;
height: $ibo-button-separator--height;
margin: $ibo-button-separator--margin-y $ibo-button-separator--margin-x;
}

View File

@@ -37,12 +37,6 @@ $ibo-field--enable-bulk--border-radius: $ibo-border-radius-500 !default;
$ibo-field--enable-bulk--checkbox--margin-left: $ibo-spacing-300 !default;
$ibo-field--label--action--margin-y: $ibo-spacing-0 !default;
$ibo-field--label--action--margin-x: $ibo-spacing-100 !default;
$ibo-field--label--action--size: $ibo-field--fullscreen-toggler--size !default;
$ibo-field--label--action--padding: $ibo-spacing-0 !default;
$ibo-field--label--action--font-size: $ibo-font-size-50 !default;
/* SCSS rules */
.ibo-field {
@extend %ibo-font-size-150;
@@ -270,32 +264,4 @@ $ibo-field--label--action--font-size: $ibo-font-size-50 !default;
.ibo-input-select--action-buttons a {
@extend %ibo-hyperlink-inherited-colors;
}
.ibo-field--label {
.ibo-field--action:not(.ibo-button-separator) {
display: inline-flex;
align-content: center;
align-items: center;
justify-content: center;
vertical-align: middle;
width: $ibo-field--label--action--size;
height: $ibo-field--label--action--size;
padding: $ibo-field--label--action--padding;
border-radius: $ibo-field--fullscreen-toggler--border-radius;
font-size: $ibo-field--label--action--font-size;
~ .ibo-field--action {
margin-left: $ibo-field--label--action--margin-x;
}
}
.ibo-field--action.ibo-button-separator {
margin: $ibo-field--label--action--margin-y $ibo-field--label--action--margin-x;
vertical-align: middle;
}
}
// Specific rule to ensure that the grouped actions popover of a field is visible in fullscreen mode
body.ibo-has-fullscreen-descendant .ibo-popover-menu.ibo-field--grouped-actions-popover {
z-index: 1060 !important;
}

View File

@@ -8,74 +8,70 @@
padding-bottom: 14px;
}
// WIP SDK Forms
//form[is="itop-form-element"] {
//
// .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;
// }
//
//}
.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;
}

View File

@@ -21,7 +21,6 @@ $ibo-input-set--item--padding-x: 6px !default;
$ibo-input-set--item--padding-y: $ibo-spacing-200 !default;
$ibo-input-set--item--max-width: 360px !default;
$ibo-input-set--item--background-color: $ibo-color-white-100 !default;
$ibo-input-set--item--color: $ibo-color-grey-900 !default;
$ibo-input-set--item--border: none !default;
$ibo-input-set--item--border-radius: $ibo-border-radius-300 !default;
$ibo-input-set--item--box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 0 1px 1px rgba(241, 241, 241, 0.7) !default;
@@ -60,7 +59,7 @@ $ibo-input-set--item--box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 0 1px 1px rgb
border-radius: $ibo-input-set--item--border-radius;
box-shadow: $ibo-input-set--item--box-shadow;
color: $ibo-input-set--item--color;
color: $ibo-color-grey-900;
text-shadow: none;
@extend %ibo-text-truncated-with-ellipsis;
/* - Guaranty that if a set item has html sub-node, they correctly display ellipsis (eg. Link set set items) */

View File

@@ -4,6 +4,7 @@
*/
@import "navigation-menu";
@import "top-bar";
@import "content";
@import "details";
@import "tab-container/tab-container";
@@ -14,5 +15,4 @@
@import "wizard-container/wizard-container";
@import "object/all";
@import "activity-panel/all";
@import "extension/all";
@import "top-bar/all";
@import "extension/all";

View File

@@ -793,9 +793,8 @@ $ibo-navigation-menu--user-info--height--is-expanded: 100% !default;
color: $ibo-navigation-menu--user--text-color;
}
.ibo-navigation-menu--user-notifications--toggler--icon {
position: relative;
padding-left: $ibo-navigation-menu--user-notifications--toggler--icon--padding-left;
.ibo-navigation-menu--user-notifications--toggler--icon {
padding-left: $ibo-navigation-menu--user-notifications--toggler--icon--padding-left;
}
}

View File

@@ -1,78 +0,0 @@
$ibo-activity-panel--activity-actions--right: $ibo-activity-panel--add-caselog-entry-button--right !default;
$ibo-activity-panel--activity-actions--top: 76px + $ibo-activity-panel--activity-actions--right !default;
$ibo-activity-panel--activity-actions--gap: $ibo-spacing-300 !default;
$ibo-activity-panel--activity-action--diameter: $ibo-activity-panel--add-caselog-entry-button--diameter !default;
$ibo-activity-panel--activity-action--background-color: $ibo-activity-panel--add-caselog-entry-button--background-color !default;
$ibo-activity-panel--activity-action--background-color--on-hover: $ibo-activity-panel--add-caselog-entry-button--background-color--on-hover !default;
$ibo-activity-panel--activity-action--background-color--is-active: $ibo-activity-panel--add-caselog-entry-button--background-color--is-active !default;
$ibo-activity-panel--activity-action--color: $ibo-activity-panel--add-caselog-entry-button--color !default;
$ibo-activity-panel--activity-action--border-radius: $ibo-border-radius-full !default;
$ibo-activity-panel--activity-action--box-shadow: $ibo-activity-panel--add-caselog-entry-button--box-shadow !default;
$ibo-activity-panel--activity-action--hover--box-shadow: $ibo-activity-panel--add-caselog-entry-button--hover--box-shadow !default;
$ibo-activity-panel--activity-action--icon--height: $ibo-activity-panel--add-caselog-entry-button--icon--height !default;
$ibo-activity-panel--activity-action--icon--width: $ibo-activity-panel--activity-action--icon--height !default;
$ibo-activity-panel--activity-action--icon--font-size: $ibo-activity-panel--add-caselog-entry-button--icon--font-size !default;
$ibo-activity-panel--activity-action--icon--line-height: $ibo-activity-panel--add-caselog-entry-button--icon--line-height !default;
$ibo-activity-panel--activity-action-separator--width: calc(#{$ibo-activity-panel--activity-action--diameter} / 1.5) !default;
$ibo-activity-panel--activity-action-separator--height: 2px !default;
$ibo-activity-panel--activity-action-separator--background-color: $ibo-color-grey-500 !default;
.ibo-activity-panel--activity-actions {
display: flex;
flex-direction: column;
gap: $ibo-activity-panel--activity-actions--gap;
position: absolute;
z-index: 1;
right: $ibo-activity-panel--activity-actions--right;
top: $ibo-activity-panel--activity-actions--top;
&.ibo-is-hidden{
display: none;
}
&:has(+ .ibo-activity-panel--add-caselog-entry-button) {
top: calc(#{$ibo-activity-panel--activity-actions--top} + #{$ibo-activity-panel--activity-action--diameter} + #{$ibo-activity-panel--activity-actions--gap});
}
}
.ibo-activity-panel--activity-action {
@extend %ibo-baseline-centered-content;
width: $ibo-activity-panel--activity-action--diameter;
height: $ibo-activity-panel--activity-action--diameter;
background-color: $ibo-activity-panel--activity-action--background-color;
color: $ibo-activity-panel--activity-action--color;
border-radius: $ibo-activity-panel--activity-action--border-radius;
box-shadow: $ibo-activity-panel--activity-action--box-shadow;
> i{
text-align: center;
height: $ibo-activity-panel--activity-action--icon--height;
width: $ibo-activity-panel--activity-action--icon--width;
font-size: $ibo-activity-panel--activity-action--icon--font-size;
line-height: $ibo-activity-panel--activity-action--icon--line-height;
}
&:hover {
color: $ibo-activity-panel--activity-action--color;
background-color: $ibo-activity-panel--activity-action--background-color--on-hover;
box-shadow: $ibo-activity-panel--activity-action--hover--box-shadow;
}
&:active {
color: $ibo-activity-panel--activity-action--color;
background-color: $ibo-activity-panel--activity-action--background-color--is-active;
}
&.ibo-is-hidden{
display: none;
}
}
.ibo-activity-panel--activity-action-separator {
display: block;
width: $ibo-activity-panel--activity-action-separator--width;
height: $ibo-activity-panel--activity-action-separator--height;
margin: auto;
background-color: $ibo-activity-panel--activity-action-separator--background-color;
}

View File

@@ -137,8 +137,7 @@ $ibo-activity-panel--open-icon--margin-left: 0.75rem !default;
.ibo-activity-panel--header,
.ibo-activity-panel--body,
.ibo-activity-panel--add-caselog-entry-button,
.ibo-activity-panel--activity-actions{
.ibo-activity-panel--add-caselog-entry-button {
display: none;
}

View File

@@ -10,4 +10,3 @@
@import "transition-entry";
@import "edits-entry";
@import "notification-entry";
@import "activity-action";

View File

@@ -59,9 +59,4 @@ $ibo-caselog-entry-form--lock-message--margin-left: 1rem !default;
}
.ibo-caselog-entry-form--action-buttons--main-actions > .ibo-popover-menu {
z-index: 1;
}
.ibo-caselog-entry-form--action-buttons--extra-actions{
overflow: hidden;
flex-grow: 1;
}

View File

@@ -1,2 +0,0 @@
@import "top-bar";
@import "top-bar-action";

View File

@@ -1,40 +0,0 @@
$ibo-top-bar-quick-action--head--background-color: $ibo-color-white-100 !default;
$ibo-top-bar-quick-action--icon-padding-x: $ibo-spacing-500 !default;
$ibo-top-bar-quick-action--icon-padding-y: $ibo-spacing-0 !default;
$ibo-top-bar-quick-action--icon--color: $ibo-color-primary-600 !default;
$ibo-top-bar-quick-action--icon--color--on-hover: $ibo-color-primary-700 !default;
$ibo-top-bar-quick-action--icon--color--on-active: $ibo-color-primary-800 !default;
$ibo-top-bar-quick-action-separator--width: 2px !default;
$ibo-top-bar-quick-action-separator--background-color: $ibo-color-grey-300 !default;
$ibo-top-bar-quick-action-separator--margin-y: $ibo-spacing-0 !default;
$ibo-top-bar-quick-action-separator--margin-x: $ibo-spacing-300 !default;
.ibo-top-bar-quick-action{
position: relative;
@extend %ibo-full-height-content;
}
.ibo-top-bar-quick-action--head{
@extend %ibo-full-height-content;
height: 100%;
background-color: $ibo-top-bar-quick-action--head--background-color;
}
.ibo-top-bar-quick-action--icon{
color: $ibo-top-bar-quick-action--icon--color;
align-self: center;
padding: $ibo-top-bar-quick-action--icon-padding-y $ibo-top-bar-quick-action--icon-padding-x;
@extend %ibo-font-ral-nor-400;
&:hover{
color: $ibo-top-bar-quick-action--icon--color--on-hover;
}
&:active{
color: $ibo-top-bar-quick-action--icon--color--on-active;
}
}
.ibo-top-bar-quick-action-separator {
width: $ibo-top-bar-quick-action-separator--width;
background-color: $ibo-top-bar-quick-action-separator--background-color;
margin: $ibo-top-bar-quick-action-separator--margin-y $ibo-top-bar-quick-action-separator--margin-x;
}

View File

@@ -70,7 +70,7 @@ $ibo-vendors-ckeditor--ck-mentions--item--padding-y: $ibo-spacing-200 !default;
}
}
.ck-editor__editable_inline:not(.ck-comment__input *), .ck-source-editing-area {
.ck-editor__editable_inline:not(.ck-comment__input *) {
height: 200px;
}
@@ -84,6 +84,11 @@ $ibo-vendors-ckeditor--ck-mentions--item--padding-y: $ibo-spacing-200 !default;
}
}
// N°7552 Allow source editing area to be scrollable in full screen
.ck-maximize_editor_main .ck-source-editing-area textarea{
overflow: auto !important;
}
/* Mentions */
.ck-mentions {
.ck-button {

View File

@@ -4,7 +4,7 @@
*/
/* SCSS variables */
$common-has-description--content: "?" !default;
$common-has-description--content: "\1F6C8" !default;
$common-has-description--padding-left: $common-spacing-200 !default;
$common-has-description--color: $common-color-grey-600 !default;
$common-has-description--font-size: 0.7em !default; /* Font size is em on purpose as we want it to be proportional to its context */

View File

@@ -11,9 +11,6 @@
*
* Usage: @extend %common-font-size-XXX;
*/
%common-font-size-20 {
font-size: $common-font-size-20;
}
%common-font-size-50 {
font-size: $common-font-size-50;
}

File diff suppressed because one or more lines are too long

View File

@@ -6,9 +6,8 @@
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'CAS:Error:UserNotAllowed' => '用户被禁止登录',
'CAS:Login:SignIn' => '使用 CAS 登录',
'CAS:Login:SignInTooltip' => '点击这里使用 CAS 服务器认证',
'CAS:Login:SignIn' => '使用CAS登录',
'CAS:Login:SignInTooltip' => '点击这里使用CAS服务器认证',
]);

View File

@@ -5,8 +5,10 @@
*
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
* @author Robert Deng <denglx@gmail.com>
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -21,7 +23,6 @@
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
// Dictionnay conventions
// Class:<class_name>
// Class:<class_name>+
@@ -31,11 +32,9 @@
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
// Class:<class_name>/Stimulus:<stimulus_code>
// Class:<class_name>/Stimulus:<stimulus_code>+
//
// Class: UserExternal
//
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:UserExternal' => '外部用户',
'Class:UserExternal+' => '用户在'.ITOP_APPLICATION_SHORT.'外部验证身份',

View File

@@ -14,7 +14,7 @@
Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:UserLDAP' => 'Пользователь LDAP',
'Class:UserLDAP+' => 'Пользователь, аутентифицируемый через LDAP',
'UserLDAP:server' => 'Особенности LDAP',
'UserLDAP:server' => 'LDAP specifics~~',
]);
//
@@ -22,6 +22,6 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
//
Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:UserLDAP/Attribute:ldap_server' => 'Сервер LDAP',
'Class:UserLDAP/Attribute:ldap_server+' => '',
'Class:UserLDAP/Attribute:ldap_server' => 'Ldap server~~',
'Class:UserLDAP/Attribute:ldap_server+' => '~~',
]);

View File

@@ -6,6 +6,7 @@
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
* @author Robert Deng <denglx@gmail.com>
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
@@ -21,7 +22,6 @@
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
// Dictionnay conventions
// Class:<class_name>
// Class:<class_name>+
@@ -31,15 +31,13 @@
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
// Class:<class_name>/Stimulus:<stimulus_code>
// Class:<class_name>/Stimulus:<stimulus_code>+
//
// Class: UserLDAP
//
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:UserLDAP' => 'LDAP 用户',
'Class:UserLDAP+' => '用户身份由 LDAP 认证',
'UserLDAP:server' => 'LDAP 详情',
'Class:UserLDAP' => 'LDAP用户',
'Class:UserLDAP+' => '用户身份由LDAP认证',
'UserLDAP:server' => 'LDAP详情',
]);
//
@@ -47,6 +45,6 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
//
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:UserLDAP/Attribute:ldap_server' => 'LDAP 服务器',
'Class:UserLDAP/Attribute:ldap_server+' => '',
'Class:UserLDAP/Attribute:ldap_server' => 'Ldap server~~',
'Class:UserLDAP/Attribute:ldap_server+' => '~~',
]);

View File

@@ -25,7 +25,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
'Class:UserLocal/Attribute:expiration/Value:force_expire' => 'abgelaufen',
'Class:UserLocal/Attribute:expiration/Value:force_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:otp_expire' => 'einmaliges Passwort',
'Class:UserLocal/Attribute:expiration/Value:otp_expire+' => 'Das Passwort kann vom Benutzer nicht geändert werden.',
'Class:UserLocal/Attribute:expiration/Value:otp_expire+' => '',
'Class:UserLocal/Attribute:password_renewed_date' => 'Letzte Passworterneuerung',
'Class:UserLocal/Attribute:password_renewed_date+' => 'Letztes Änderungsdatum',
'Error:UserLocalPasswordValidator:UserPasswordPolicyRegex:ValidationFailed' => 'Das Passwort muss mindestens 12 Zeichen lang sein und Großbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten.',

View File

@@ -24,11 +24,11 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:UserLocal/Attribute:expiration/Value:never_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:force_expire' => 'Истёкший',
'Class:UserLocal/Attribute:expiration/Value:force_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:otp_expire' => 'Одноразовый пароль',
'Class:UserLocal/Attribute:expiration/Value:otp_expire+' => 'Пароль не может быть изменён пользователем.',
'Class:UserLocal/Attribute:expiration/Value:otp_expire' => 'One-time Password~~',
'Class:UserLocal/Attribute:expiration/Value:otp_expire+' => 'Password cannot be changed by the user.~~',
'Class:UserLocal/Attribute:password_renewed_date' => 'Дата изменения пароля',
'Class:UserLocal/Attribute:password_renewed_date+' => 'Когда пароль был изменен в последний раз',
'Error:UserLocalPasswordValidator:UserPasswordPolicyRegex:ValidationFailed' => 'Пароль должен содержать не менее 12 символов и включать прописные, строчные, числовые и специальные символы.',
'UserLocal:password:expiration' => 'Поля требуют наличия доп. расширения',
'Class:UserLocal/Error:OneTimePasswordChangeIsNotAllowed' => 'Установка срока действия пароля "Одноразовый пароль" для своей собственной учётной записи не разрешена',
'Class:UserLocal/Error:OneTimePasswordChangeIsNotAllowed' => 'Setting password expiration to "One-time password" is not allowed for your own User~~',
]);

View File

@@ -6,6 +6,7 @@
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
* @author Robert Deng <denglx@gmail.com>
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
@@ -21,7 +22,6 @@
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
// Dictionnay conventions
// Class:<class_name>
// Class:<class_name>+
@@ -31,19 +31,16 @@
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
// Class:<class_name>/Stimulus:<stimulus_code>
// Class:<class_name>/Stimulus:<stimulus_code>+
//
// Class: UserLocal
//
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:UserLocal' => ITOP_APPLICATION_SHORT.' 用户',
'Class:UserLocal+' => '用户由'.ITOP_APPLICATION_SHORT.'验证身份',
'Class:UserLocal/Attribute:password' => '密码',
'Class:UserLocal/Attribute:password+' => '用于验证用户身份的字符串',
'Class:UserLocal/Attribute:expiration' => '密码过期时间',
'Class:UserLocal/Attribute:expiration+' => '密码过期状态 (需要扩展才能生效)',
'Class:UserLocal/Attribute:expiration' => '密码过期',
'Class:UserLocal/Attribute:expiration+' => '密码过期状态 (需要一个扩展才能生效)',
'Class:UserLocal/Attribute:expiration/Value:can_expire' => '允许过期',
'Class:UserLocal/Attribute:expiration/Value:can_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:never_expire' => '永不过期',
@@ -52,9 +49,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:UserLocal/Attribute:expiration/Value:force_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:otp_expire' => '一次性密码',
'Class:UserLocal/Attribute:expiration/Value:otp_expire+' => '用户不允许修改密码.',
'Class:UserLocal/Attribute:password_renewed_date' => '密码更新时间',
'Class:UserLocal/Attribute:password_renewed_date' => '密码更新',
'Class:UserLocal/Attribute:password_renewed_date+' => '上次修改密码的时间',
'Error:UserLocalPasswordValidator:UserPasswordPolicyRegex:ValidationFailed' => '密码必须至少12个字符, 包含大小写, 数字和特殊字符.',
'UserLocal:password:expiration' => '下面的区域需要插件扩展',
'Class:UserLocal/Error:OneTimePasswordChangeIsNotAllowed' => '不允许用户为自己设置 "一次性密码" 的失效期限',

View File

@@ -11,5 +11,5 @@
*
*/
Dict::Add('RU RU', 'Russian', 'Русский', [
'theme:darkmoon' => 'Тёмная луна',
'theme:darkmoon' => 'Dark moon~~',
]);

View File

@@ -21,7 +21,6 @@
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'theme:darkmoon' => 'Dark moon',
]);

View File

@@ -149,8 +149,6 @@ $ibo-quick-create--input--text-color: $ibo-color-grey-100 !default;
$ibo-quick-create--input--placeholder-color: $ibo-color-grey-200 !default;
$ibo-quick-create--compartment-element--background-color--is-active: $ibo-color-grey-500 !default;
$ibo-top-bar-quick-action--head--background-color: $ibo-top-bar--background-color !default;
$ibo-datatable--toolbar--text-color: $ibo-color-grey-300 !default;
$ibo-vendors-datatables--pagination-button--background-color--on-hover: $ibo-color-grey-700 !default;
$ibo-vendors-datatables--pagination-button--text-color--is-active: $ibo-color-grey-200 !default;
@@ -202,10 +200,6 @@ $ibo-input-select-selectize--item--active--text-color: $ibo-color-grey-100 !defa
$ibo-input-select-selectize--item--active--background-color: $ibo-color-grey-500 !default;
$ibo-input-select--autocomplete-item-image--background-color: $ibo-color-grey-800 !default;
$ibo-input-date--button--color: $ibo-input-select--action-button--color;
$ibo-input-set--item--background-color: $ibo-color-grey-600 !default;
$ibo-input-set--item--color: $ibo-color-grey-200 !default;
$ibo-input-set--item--box-shadow: none !default;
$ibo-input-set--item--border: solid 1px $ibo-color-grey-800 !default;
$ibo-vendors-selectize-input--color: $ibo-body-text-color !default;
$ibo-vendors-selectize-input--background-color: $ibo-input--background-color !default;
$ibo-vendors-selectize--input--border-color: $ibo-input--border-color !default;

View File

@@ -23,5 +23,5 @@
*/
Dict::Add('DE DE', 'German', 'Deutsch', [
'theme:fullmoon-high-contrast' => 'Fullmoon (Hoher Kontrast)',
'theme:fullmoon-high-contrast' => 'Fullmoon (High contrast)~~',
]);

View File

@@ -23,5 +23,5 @@
*/
Dict::Add('RU RU', 'Russian', 'Русский', [
'theme:fullmoon-high-contrast' => 'Fullmoon (высокая контрастность)',
'theme:fullmoon-high-contrast' => 'Fullmoon (High contrast)~~',
]);

View File

@@ -23,5 +23,5 @@
*/
Dict::Add('DE DE', 'German', 'Deutsch', [
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (Protanopie & Deuteranopie)',
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (Protanopia & Deuteranopia)~~',
]);

View File

@@ -23,5 +23,5 @@
*/
Dict::Add('RU RU', 'Russian', 'Русский', [
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (протанопия и дейтеранопия)',
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (Protanopia & Deuteranopia)~~',
]);

View File

@@ -23,5 +23,5 @@
*/
Dict::Add('DE DE', 'German', 'Deutsch', [
'theme:fullmoon-tritanopia' => 'Fullmoon (Tritanopie)',
'theme:fullmoon-tritanopia' => 'Fullmoon (Tritanopia)~~',
]);

View File

@@ -23,5 +23,5 @@
*/
Dict::Add('RU RU', 'Russian', 'Русский', [
'theme:fullmoon-tritanopia' => 'Fullmoon (тританопия)',
'theme:fullmoon-tritanopia' => 'Fullmoon (Tritanopia)~~',
]);

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<service_locator>
<services>
<service id="DataFeatureRemoverExtension">
<class id="Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService"/>
</service>
</services>
</service_locator>

View File

@@ -1,24 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.3">
<classes>
<class id="ResourceDataFeatureRemoval" _delta="define">
<parent>AbstractResource</parent>
<properties>
<comment>/* Extension management access control. */</comment>
<abstract>true</abstract>
<category>grant_by_profile</category>
</properties>
<presentation/>
<methods/>
</class>
</classes>
<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_class>ResourceDataFeatureRemoval</enable_class>
<enable_action>UR_ACTION_MODIFY</enable_action>
<enable_admin_only>1</enable_admin_only>
</menu>
</menus>
<module_parameters>

View File

@@ -1,71 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2026 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Localized data
*/
Dict::Add('DE DE', 'German', 'Deutsch', [
'Menu:DataFeatureRemovalMenu' => 'Extension-Verwaltung',
'combodo-data-feature-removal/Operation:Main/Title' => 'Extension-Verwaltung',
'DataFeatureRemoval:Main:Title' => 'Extension-Verwaltung',
'DataFeatureRemoval:Main:SubTitle' => 'In Ihrem iTop installierte Extensions aktivieren oder deaktivieren',
'DataFeatureRemoval:Failure:Title' => 'Fehler beim Testlauf der Extension-Entfernung',
'DataFeatureRemoval:Helper:Title' => 'Prüfen Sie, ob Daten oder Abhängigkeiten dem Hinzufügen bzw. Entfernen einer Extension entgegenstehen.',
'DataFeatureRemoval:Features:Title' => 'Extensions',
'DataFeatureRemoval:Result:Title' => 'Angeforderte Änderung',
'DataFeatureRemoval:NoResult:Title' => 'Keine Änderung angefordert',
'DataFeatureRemoval:Execution:Title' => 'Ausführung der Löschvorgänge',
'DataFeatureRemoval:Analysis:Title' => 'Analyseergebnis',
'DataFeatureRemoval:Analysis:Subtitle' => 'Prüfen Sie alle Elemente, die Ihre Aufmerksamkeit erfordern',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s Element(e) müssen bereinigt werden, bevor Sie fortfahren können',
'DataFeatureRemoval:DeletionPlan:Title' => 'Plan zur Datenlöschung',
'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s Zeilen müssen bereinigt werden, bevor Sie fortfahren können',
'DataFeatureRemoval:DoDeletion:Title' => 'Löschung durchführen',
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Alle Einträge aus der Datenbank entfernen',
'DataFeatureRemoval:DeletionPlan:Error:Issues' => 'Einige Objekte müssen vor der Bereinigung manuell gelöscht werden',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Zu entfernendes Element',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Name der Extension',
'DataFeatureRemoval:Table:Analysis:Module' => 'Modulname',
'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Vorkommen',
'DataFeatureRemoval:CleanupComplete:Title' => 'Alles bereinigt.',
'DataFeatureRemoval:CompilComplete' => 'Kompilierung erfolgreich. Keine Bereinigung erforderlich. Sie können mit dem Setup fortfahren.',
'DataFeatureRemoval:Compile:InProgress' => 'Kompilierung läuft...',
'DataFeatureRemoval:Compile:Success' => 'Kompilierung erfolgreich',
'DataFeatureRemoval:Compile:Error' => 'Fehler bei der Kompilierung',
'DataFeatureRemoval:RunAudit:InProgress' => 'Analyse läuft...',
'DataFeatureRemoval:RunAudit:Success' => 'Analyse abgeschlossen',
'DataFeatureRemoval:RunAudit:Error' => 'Fehler während der Analyse',
'UI:Button:Analyze' => 'Analysieren',
'UI:Button:ModifyChoices' => 'Auswahl ändern',
'UI:Button:AnalyzeAndSetup' => 'Analysieren und zum Setup',
'UI:Button:PlanDeletion' => 'Mit der Löschung fortfahren',
'UI:Button:DoDeletion' => 'Mit der Löschung fortfahren',
'UI:Button:BackToMain' => 'Auswahl ändern',
'UI:Button:Setup' => 'Setup ausführen',
'UI:Action:ForceUninstall' => 'Deinstallation erzwingen',
'UI:Action:MoreInfo' => 'Weitere Informationen',
'DataFeatureRemoval:Table:Empty' => 'Keine zu entfernenden Daten',
'DataFeatureRemoval:Column:Class' => 'Klasse',
'DataFeatureRemoval:Column:DeleteCount' => 'Zu löschende Einträge',
'DataFeatureRemoval:Column:UpdateCount' => 'Zu aktualisierende Einträge',
'DataFeatureRemoval:Column:IssueCount' => 'Gefundene Probleme, die eine automatische Bereinigung verhindern',
'DataFeatureRemoval:Column:DeletedCount' => 'Gelöschte Einträge',
'DataFeatureRemoval:Column:UpdatedCount' => 'Aktualisierte Einträge',
]);

View File

@@ -20,7 +20,6 @@ Dict::Add('EN US', 'English', 'English', [
'DataFeatureRemoval:Features:Title' => 'Extensions',
'DataFeatureRemoval:Result:Title' => 'Modification requested',
'DataFeatureRemoval:NoResult:Title' => 'No modification requested',
'DataFeatureRemoval:Execution:Title' => 'Deletion Executions',
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
'DataFeatureRemoval:Analysis:Subtitle' => 'Review all elements requiring attention',
@@ -40,14 +39,6 @@ Dict::Add('EN US', 'English', 'English', [
'DataFeatureRemoval:CleanupComplete:Title' => 'All clear.',
'DataFeatureRemoval:CompilComplete' => 'Compilation successful. No Cleanup needed. You can proceed to setup.',
'DataFeatureRemoval:Compile:InProgress' => 'Compilation in progress...',
'DataFeatureRemoval:Compile:Success' => 'Compilation successful',
'DataFeatureRemoval:Compile:Error' => 'Compilation error',
'DataFeatureRemoval:RunAudit:InProgress' => 'Analysis in progress...',
'DataFeatureRemoval:RunAudit:Success' => 'Analysis completed',
'DataFeatureRemoval:RunAudit:Error' => 'Error during analysis',
'UI:Button:Analyze' => 'Analyze',
'UI:Button:ModifyChoices' => 'Change my selection',
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',

View File

@@ -1,19 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2026 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Localized data
*
* Only the entries whose spelling differs from EN US are listed here; every
* other string falls back to the EN US dictionary.
*/
Dict::Add('EN GB', 'British English', 'British English', [
'DataFeatureRemoval:Helper:Title' => 'Analyse if there are any data or dependency preventing you from adding/removing an extension.',
'UI:Button:Analyze' => 'Analyse',
'UI:Button:AnalyzeAndSetup' => 'Analyse and go to setup',
]);

View File

@@ -20,7 +20,6 @@ Dict::Add('FR FR', 'French', 'Français', [
'DataFeatureRemoval:Features:Title' => 'Extensions',
'DataFeatureRemoval:Result:Title' => 'Modification demandée',
'DataFeatureRemoval:NoResult:Title' => 'Aucune modification demandée',
'DataFeatureRemoval:Execution:Title' => 'Suppressions',
'DataFeatureRemoval:Analysis:Title' => 'Résultat de l’analyse',
'DataFeatureRemoval:Analysis:Subtitle' => 'Vérifier les éléments à nettoyer',
@@ -40,14 +39,6 @@ Dict::Add('FR FR', 'French', 'Français', [
'DataFeatureRemoval:CleanupComplete:Title' => 'All clear.',
'DataFeatureRemoval:CompilComplete' => 'Compilation successful. No Cleanup needed. You can proceed to setup.',
'DataFeatureRemoval:Compile:InProgress' => 'Compilation en cours...',
'DataFeatureRemoval:Compile:Success' => 'Compilation terminée',
'DataFeatureRemoval:Compile:Error' => 'Erreur lors de la compilation',
'DataFeatureRemoval:RunAudit:InProgress' => 'Analyse en cours...',
'DataFeatureRemoval:RunAudit:Success' => 'Analyse terminée',
'DataFeatureRemoval:RunAudit:Error' => 'Erreur lors de l\'analyse',
'UI:Button:Analyze' => 'Analyser',
'UI:Button:ModifyChoices' => 'Modifier la sélection',
'UI:Button:AnalyzeAndSetup' => 'Analyser et ouvrir l’assistant de configuration',

View File

@@ -1,75 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Localized data
*/
/**
* @author Vladimir Kunin <v.b.kunin@gmail.com>
*
*/
Dict::Add('RU RU', 'Russian', 'Русский', [
'Menu:DataFeatureRemovalMenu' => 'Управление расширениями',
'combodo-data-feature-removal/Operation:Main/Title' => 'Управление расширениями',
'DataFeatureRemoval:Main:Title' => 'Управление расширениями',
'DataFeatureRemoval:Main:SubTitle' => 'Включение и отключение расширений, установленных в вашем iTop',
'DataFeatureRemoval:Failure:Title' => 'Ошибки пробного удаления расширений',
'DataFeatureRemoval:Helper:Title' => 'Проверьте, есть ли данные или зависимости, мешающие добавить/удалить расширение.',
'DataFeatureRemoval:Features:Title' => 'Расширения',
'DataFeatureRemoval:Result:Title' => 'Запрошено изменение',
'DataFeatureRemoval:NoResult:Title' => 'Изменений не запрошено',
'DataFeatureRemoval:Execution:Title' => 'Выполнения удаления',
'DataFeatureRemoval:Analysis:Title' => 'Результат анализа',
'DataFeatureRemoval:Analysis:Subtitle' => 'Просмотрите все элементы, требующие внимания',
'DataFeatureRemoval:Analysis:SubTitle' => 'Элементов для очистки перед продолжением: %1$s',
'DataFeatureRemoval:DeletionPlan:Title' => 'План удаления данных',
'DataFeatureRemoval:DeletionPlan:SubTitle' => 'Строк для очистки перед продолжением: %1$s',
'DataFeatureRemoval:DoDeletion:Title' => 'Выполнить удаление',
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Удалить все записи из базы данных',
'DataFeatureRemoval:DeletionPlan:Error:Issues' => 'Некоторые объекты нужно удалить вручную перед очисткой',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Элемент для удаления',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Название расширения',
'DataFeatureRemoval:Table:Analysis:Module' => 'Название модуля',
'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Количество',
'DataFeatureRemoval:CleanupComplete:Title' => 'Всё чисто.',
'DataFeatureRemoval:CompilComplete' => 'Компиляция выполнена успешно. Очистка не требуется. Можно переходить к установке.',
'DataFeatureRemoval:Compile:InProgress' => 'Идёт компиляция...',
'DataFeatureRemoval:Compile:Success' => 'Компиляция выполнена успешно',
'DataFeatureRemoval:Compile:Error' => 'Ошибка компиляции',
'DataFeatureRemoval:RunAudit:InProgress' => 'Идёт анализ...',
'DataFeatureRemoval:RunAudit:Success' => 'Анализ завершён',
'DataFeatureRemoval:RunAudit:Error' => 'Ошибка при анализе',
'UI:Button:Analyze' => 'Анализировать',
'UI:Button:ModifyChoices' => 'Изменить выбор',
'UI:Button:AnalyzeAndSetup' => 'Анализировать и перейти к установке',
'UI:Button:PlanDeletion' => 'Продолжить удаление',
'UI:Button:DoDeletion' => 'Продолжить удаление',
'UI:Button:BackToMain' => 'Изменить выбор',
'UI:Button:Setup' => 'Запустить установку',
'UI:Action:ForceUninstall' => 'Принудительно удалить',
'UI:Action:MoreInfo' => 'Подробнее',
'DataFeatureRemoval:Table:Empty' => 'Нет данных для удаления',
'DataFeatureRemoval:Column:Class' => 'Класс',
'DataFeatureRemoval:Column:DeleteCount' => 'Записей к удалению',
'DataFeatureRemoval:Column:UpdateCount' => 'Записей к обновлению',
'DataFeatureRemoval:Column:IssueCount' => 'Найдено проблем, мешающих автоматической очистке',
'DataFeatureRemoval:Column:DeletedCount' => 'Удалено записей',
'DataFeatureRemoval:Column:UpdatedCount' => 'Обновлено записей',
]);

View File

@@ -1,62 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Localized data
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Menu:DataFeatureRemovalMenu' => '扩展管理',
'combodo-data-feature-removal/Operation:Main/Title' => '扩展管理',
'DataFeatureRemoval:Main:Title' => '扩展管理',
'DataFeatureRemoval:Main:SubTitle' => '切换安装在您的 iTop 上的扩展',
'DataFeatureRemoval:Failure:Title' => '扩展预删除错误',
'DataFeatureRemoval:Helper:Title' => '分析是否有任何数据或依赖关系阻止您添加/删除扩展。',
'DataFeatureRemoval:Features:Title' => '扩展',
'DataFeatureRemoval:Result:Title' => '请求的修改',
'DataFeatureRemoval:Execution:Title' => '删除执行',
'DataFeatureRemoval:Analysis:Title' => '分析结果',
'DataFeatureRemoval:Analysis:Subtitle' => '审查所有需要关注的元素',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s 个元素需要在继续之前清理',
'DataFeatureRemoval:DeletionPlan:Title' => '数据删除计划',
'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s 行需要在继续之前清理',
'DataFeatureRemoval:DoDeletion:Title' => '执行删除',
'DataFeatureRemoval:DoDeletion:SubTitle' => '从数据库中删除所有条目',
'DataFeatureRemoval:DeletionPlan:Error:Issues' => '某些对象必须在清理前手动删除',
'DataFeatureRemoval:Table:Analysis:ClassName' => '要删除的元素',
'DataFeatureRemoval:Table:Analysis:FeatureName' => '扩展名称',
'DataFeatureRemoval:Table:Analysis:Module' => '模块名称',
'DataFeatureRemoval:Table:Analysis:Occurrence' => '出现次数',
'DataFeatureRemoval:CleanupComplete:Title' => '全部清除.',
'DataFeatureRemoval:CompilComplete' => '编译成功. 无需清理. 您可以继续进行设置.',
'UI:Button:Analyze' => '分析',
'UI:Button:ModifyChoices' => '改变我的选择',
'UI:Button:AnalyzeAndSetup' => '分析并进入设置',
'UI:Button:PlanDeletion' => '继续删除',
'UI:Button:DoDeletion' => '继续删除',
'UI:Button:BackToMain' => '改变我的选择',
'UI:Button:Setup' => '运行安装向导',
'UI:Action:ForceUninstall' => '强制卸载',
'UI:Action:MoreInfo' => '更多信息',
'DataFeatureRemoval:Table:Empty' => '没有数据需要删除',
'DataFeatureRemoval:Column:Class' => '类',
'DataFeatureRemoval:Column:DeleteCount' => '待删除的条目',
'DataFeatureRemoval:Column:UpdateCount' => '待更新的条目',
'DataFeatureRemoval:Column:IssueCount' => '发现阻止自动清理的问题',
'DataFeatureRemoval:Column:DeletedCount' => '已删除的条目',
'DataFeatureRemoval:Column:UpdatedCount' => '已更新的条目',
]);

View File

@@ -16,6 +16,7 @@ use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
use Combodo\iTop\DataFeatureRemoval\Service\DataCleanupService;
use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService;
use Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan;
use Combodo\iTop\Service\Session\SessionParameters;
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
@@ -23,7 +24,6 @@ use ContextTag;
use CoreException;
use Dict;
use Exception;
use iTopExtension;
use MetaModel;
use MFCompiler;
use RunTimeEnvironment;
@@ -47,11 +47,7 @@ class DataFeatureRemovalController extends Controller
$aParams = [];
SetupUtils::EraseSetupToken();
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
$oParameters->Erase();
Session::Unset('aDeletionExecutionSummary');
Session::Set('bForceCompilation', true);
$oParameters->SetParameter('return_application', 'DataFeatureRemoval');
(new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME))->Erase();
$this->AddAnalyzeParams();
$aParams['sTransactionId'] = utils::GetNewTransactionId();
@@ -64,6 +60,7 @@ class DataFeatureRemovalController extends Controller
$aParams['sSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup';
$aParams['iCount'] = $this->iCount;
Session::Set('bForceCompilation', true);
$this->AddLinkedStylesheet(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/css/DataFeatureRemoval.css');
$this->AddLinkedScript(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/js/DataFeatureRemoval.js');
$this->DisplayPage($aParams);
@@ -76,7 +73,7 @@ class DataFeatureRemovalController extends Controller
$this->iCount = 0;
foreach ($this->aCountClassesToCleanup as $sClass => $iCount) {
$sModuleName = MetaModel::GetModuleName($sClass);
$aExtensions = Metamodel::GetServiceLocator()->Get('DataFeatureRemoverExtension')->GetIncludingExtensions($sModuleName);
$aExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetIncludingExtensions($sModuleName);
$sExtensions = implode(' ', $aExtensions);
$aColumns = ['ClassName','FeatureName','Module','Occurrence'];
$aData[] = [$sClass,$sExtensions,$sModuleName,$iCount];
@@ -97,7 +94,7 @@ class DataFeatureRemovalController extends Controller
}
// Display changed extensions
$aSetupParameterNames = [
$aHiddenInputNames = [
'selected_extensions' => '[]',
'selected_modules' => '[]',
'display_choices' => '',
@@ -111,38 +108,92 @@ class DataFeatureRemovalController extends Controller
'target_env' => ITOP_DEFAULT_ENV,
];
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
foreach ($aSetupParameterNames as $sInputName => $defaultValue) {
$oParameters->SetParameter($sInputName, $oParameters->GetParameter($sInputName, $defaultValue));
$aHiddenInputs = [];
foreach ($aHiddenInputNames as $sInputName => $defaultValue) {
$aHiddenInputs[$sInputName] = utils::ReadPostedParam($sInputName, $defaultValue, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
}
$aParams['aHiddenInputs'] = $aHiddenInputs;
$aAddedExtensions = json_decode($oParameters->GetParameter('added_extensions', '[]'), true);
$aRemovedExtensions = json_decode($oParameters->GetParameter('removed_extensions', '[]'), true);
if ('[]' === $oParameters->GetParameter('selected_modules', '[]')) {
$aAddedExtensions = json_decode($aHiddenInputs['added_extensions'], true);
$aRemovedExtensions = json_decode($aHiddenInputs['removed_extensions'], true);
if ("[]" === $aHiddenInputs['selected_modules']) {
//it does not come from setup
// we get extensions from 1st screen uiblocks
$this->ReadExtensionsDiff();
$oParameters->SetParameter('force-uninstall', $this->bForcedUninstallation ? 'on' : '');
$aHiddenInputs['force-uninstall'] = $this->bForcedUninstallation ? 'on' : '';
$aAddedExtensions = $this->aExtensionsToCheck['to_be_installed'];
$oParameters->SetParameter('added_extensions', $this->ConvertIntoSetupFormat($aAddedExtensions));
$aHiddenInputs['added_extensions'] = $this->ConvertIntoSetupFormat($aAddedExtensions);
$aRemovedExtensions = $this->aExtensionsToCheck['to_be_removed'];
$oParameters->SetParameter('removed_extensions', $this->ConvertIntoSetupFormat($aRemovedExtensions));
$aExtensionsNotUninstallable = $this->aExtensionsToCheck['extensions_not_uninstallable'];
$oParameters->SetParameter('extensions_not_uninstallable', $this->ConvertIntoSetupFormat($aExtensionsNotUninstallable));
$aHiddenInputs['removed_extensions'] = $this->ConvertIntoSetupFormat($aRemovedExtensions);
}
$aParams['aAddedExtensions'] = $aAddedExtensions;
$aParams['aRemovedExtensions'] = $aRemovedExtensions;
DataFeatureRemovalLog::Debug(__METHOD__.' Extensions given in parameter', null, [
'added_extensions' => $aAddedExtensions,
'removed_extensions' => $aRemovedExtensions]);
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['iColumnCount'] = $this->iColumnCount;
$aAvailableExtensions = $this->GetExtensionsDiff($aAddedExtensions, $aRemovedExtensions);
$aParams['aAvailableExtensionsCount'] = count($aAvailableExtensions);
$aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($aAvailableExtensions, $this->iColumnCount);
$aParams['sAjaxURL'] = utils::GetAbsoluteUrlModulePage(DataFeatureRemovalHelper::MODULE_NAME, 'index.php');
$aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($this->GetExtensionsDiff($aAddedExtensions, $aRemovedExtensions), $this->iColumnCount);
//to make setup redirection work, we need to pass complex data structures to setup wizards (ie extension/module lists)
$sSourceEnv = MetaModel::GetEnvironment();
$this->oRuntimeEnvironment = new RunTimeEnvironment($sSourceEnv, false);
if ('[]' === $aHiddenInputs['selected_modules']) {
$oConfig = MetaModel::GetConfig();
$aSelectedExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetSelectedExtensions($oConfig, array_keys($aAddedExtensions), array_keys($aRemovedExtensions));
$aHiddenInputs['selected_extensions'] = $this->ConvertIntoSetupFormat($aSelectedExtensions);
$aSelectedModules = []; // keep it to compile method
} else {
$aSelectedExtensions = json_decode($aHiddenInputs['selected_extensions'], true);
$aSelectedModules = json_decode($aHiddenInputs['selected_modules'], true);
}
try {
$this->Compile($aSelectedExtensions, array_keys($aRemovedExtensions), $aSelectedModules);
$aHiddenInputs['selected_modules'] = $this->ConvertIntoSetupFormat($aSelectedModules);
} catch (CoreException $e) {
$aParams['DataFeatureRemovalErrorMessage'] = $e->getHtmlDesc();
$this->DisplayPage($aParams, 'AnalysisResult');
return;
} catch (Exception $e) {
$aParams['DataFeatureRemovalErrorMessage'] = $e->getMessage();
$this->DisplayPage($aParams, 'AnalysisResult');
return;
}
$oSetupAudit = new SetupAudit($sSourceEnv);
$aGetRemovedClasses = array_keys($oSetupAudit->RunDataAudit());
DataFeatureRemovalLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]);
$aParams['aClasses'] = $aGetRemovedClasses;
new ContextTag(ContextTag::TAG_SETUP);
$aParams['sLaunchSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup/wizard.php';
$aParams['aSetupParams'] = [
"_class" => "WizStepLandingBeforeAudit",
"operation" => "next",
];
foreach ($aHiddenInputs as $sInputName => $sInputValue) {
$aParams['aSetupParams'][$sInputName] = $sInputValue;
}
[$aParams['aDeletionPlanSummary'], $aParams['iQueryCount'], $aParams['bDeletionPossible']] = $this->GetDeletionPlanSummaryTable($aGetRemovedClasses);
[$aParams['aDeletionExecutionSummary'], $aParams['bHasDeletionExecution']] = $this->GetExecutionSummaryTable();
$aParams['bDeletionNeeded'] = ($aParams['iQueryCount'] > 0);
Session::Set('aDeletionExecutionSummary', serialize($this->aDeletionExecutionSummary));
if (!$aParams['bDeletionNeeded']) {
// Erase session setup parameters
SetupUtils::CreateSetupToken();
}
$this->DisplayPage($aParams, 'AnalysisResult');
}
@@ -150,137 +201,55 @@ class DataFeatureRemovalController extends Controller
private function ConvertIntoSetupFormat(array $aData): string
{
$aNewData = [];
foreach ($aData as $sCode => $sLabel) {
$aNewData[] = sprintf('"%s":"%s"', $sCode, $sLabel);
foreach ($aData as $k => $sVal) {
$aNewData[] = sprintf('"%s":"%s"', $k, $sVal);
}
return "{".implode(',', $aNewData)."}";
}
/**
* @param array $aSelectedExtensions
* @param array $aRemovedExtensions
* @param array $aSelectedModules
*
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
public function OperationAjaxCompile(): void
private function Compile(array $aSelectedExtensions, array $aRemovedExtensions, array &$aSelectedModules): void
{
$aParams = [];
//to make setup redirection work, we need to pass complex data structures to setup wizards (ie extension/module lists)
$sSourceEnv = MetaModel::GetEnvironment();
$oRuntimeEnvironment = new RunTimeEnvironment($sSourceEnv, false);
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
$aSelectedModules = json_decode($oParameters->GetParameter('selected_modules', '[]'), true);
$aSelectedExtensions = json_decode($oParameters->GetParameter('selected_extensions', '[]'), true);
$aAddedExtensions = json_decode($oParameters->GetParameter('added_extensions', '[]'), true);
$aRemovedExtensions = json_decode($oParameters->GetParameter('removed_extensions', '[]'), true);
try {
$this->ValidateTransactionId();
$oConfig = MetaModel::GetConfig();
DataFeatureRemovalLog::Debug(
__METHOD__.": extension lists",
null,
['removed_extensions' => $aRemovedExtensions,
'selected_extensions' => $aSelectedExtensions,
'added_extensions' => $aAddedExtensions,
]
);
$sBuildDir = APPROOT."/env-$sSourceEnv-build";
if (! is_dir($sBuildDir)) {
SetupUtils::builddir($sBuildDir);
}
$bIsDirEmpty = count(scandir($sBuildDir)) === 2;
$bForceCompilation = Session::Get('bForceCompilation', false);
$oConfig = MetaModel::GetConfig();
if ($bIsDirEmpty || $bForceCompilation) {
Session::Unset('bForceCompilation');
$this->oRuntimeEnvironment->CopySetupFiles();
if (count($aSelectedModules) === 0) {
$aSelectedExtensions = Metamodel::GetServiceLocator()->Get('DataFeatureRemoverExtension')->GetExtensionMap()->GetSelectedExtensions($oConfig, array_keys($aAddedExtensions), array_keys($aRemovedExtensions));
$oParameters->SetParameter('selected_extensions', $this->ConvertIntoSetupFormat($aSelectedExtensions));
$aSelectedModules = $this->oRuntimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions);
}
$sBuildDir = APPROOT."/env-$sSourceEnv-build";
if (! is_dir($sBuildDir)) {
SetupUtils::builddir($sBuildDir);
DataFeatureRemovalLog::Debug(
__METHOD__,
null,
['sSourceEnv' => $sSourceEnv, 'sBuildDir' => $sBuildDir, 'bIsDirEmpty' => $bIsDirEmpty, glob("$sBuildDir/*")]
);
$this->oRuntimeEnvironment->DoCompile($aSelectedExtensions, $aRemovedExtensions, $aSelectedModules, MFCompiler::CanUseSymbolicLinks());
} else {
if (count($aSelectedModules) === 0) {
$aSelectedModules = $this->oRuntimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions);
}
$bIsDirEmpty = count(scandir($sBuildDir)) === 2;
$bForceCompilation = Session::Get('bForceCompilation', false);
if ($bIsDirEmpty || $bForceCompilation) {
$oRuntimeEnvironment->CopySetupFiles();
if (count($aSelectedModules) === 0) {
$oExtensionsMap = \iTopExtensionsMap::GetExtensionsMap($oRuntimeEnvironment->GetBuildEnv());
// Removed modules are stored as static for FindModules()
$oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensions);
$aSelectedModules = $oRuntimeEnvironment->GetModulesToLoadFromSelectedExtensions($oConfig, $aSelectedExtensions);
}
DataFeatureRemovalLog::Debug(
__METHOD__,
null,
['sSourceEnv' => $sSourceEnv, 'sBuildDir' => $sBuildDir, 'bIsDirEmpty' => $bIsDirEmpty, glob("$sBuildDir/*")]
);
$oRuntimeEnvironment->DoCompile($aSelectedExtensions, $aRemovedExtensions, $aSelectedModules, MFCompiler::CanUseSymbolicLinks());
Session::Unset('bForceCompilation');
} else {
if (count($aSelectedModules) === 0) {
$aSelectedModules = $oRuntimeEnvironment->GetModulesToLoadFromSelectedExtensions($oConfig, $aSelectedExtensions);
}
}
DataFeatureRemovalLog::Debug(__METHOD__.": modules", null, ['selected_modules' => $aSelectedModules]);
} catch (CoreException $e) {
$aParams['error_message'] = $e->getHtmlDesc();
} catch (Exception $e) {
$aParams['error_message'] = $e->getMessage();
}
$aParams['success_message'] = Dict::S('DataFeatureRemoval:Compile:Success');
$aParams['transaction_id'] = utils::GetNewTransactionId();
$oParameters->SetParameter('selected_modules', json_encode($aSelectedModules));
$this->DisplayJSONPage($aParams);
}
public function OperationAjaxRunAudit(): void
{
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
$aParams = [];
$aPageParams = [];
try {
$this->ValidateTransactionId();
$sSourceEnv = MetaModel::GetEnvironment();
$oSetupAudit = new SetupAudit($sSourceEnv);
$aRemovedClasses = array_keys($oSetupAudit->RunDataAudit());
$oParameters->SetParameter('classes', $aRemovedClasses);
new ContextTag(ContextTag::TAG_SETUP);
$aPageParams['sLaunchSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup/wizard.php';
$aPageParams['aSetupParams'] = [
"_class" => "WizStepLandingBeforeAudit",
"operation" => "next",
];
$aPageParams['sTransactionId'] = utils::GetNewTransactionId();
$aDeletionPlanSummaryEntities = $this->GetDeletionPlanSummaryEntities($aRemovedClasses);
[$aPageParams['aDeletionPlanSummary'], $aPageParams['iQueryCount'], $aPageParams['bDeletionPossible']] = $this->GetDeletionPlanSummaryTable($aDeletionPlanSummaryEntities);
[$aPageParams['aDeletionExecutionSummary'], $aPageParams['bHasDeletionExecution']] = $this->GetExecutionSummaryTable();
$aPageParams['bDeletionNeeded'] = ($aPageParams['iQueryCount'] > 0);
if (!$aPageParams['bDeletionNeeded']) {
// Erase session setup parameters
SetupUtils::CreateSetupToken();
}
$this->DisplayAjaxPage($aPageParams, 'AjaxRunAudit');
return;
} catch (CoreException $e) {
$aParams['error_message'] = $e->getHtmlDesc();
} catch (Exception $e) {
$aParams['error_message'] = $e->getMessage();
}
$this->DisplayJSONPage($aParams);
}
private function GetExecutionSummaryTable(): array
{
$sName = 'ExecutionSummary';
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
$sName = 'ExcutionSummary';
$aTableData = [];
if (count($this->aDeletionExecutionSummary) === 0) {
@@ -306,15 +275,11 @@ class DataFeatureRemovalController extends Controller
}
private function GetDeletionPlanSummaryEntities(array $aRemovedClasses): array
{
$oDataCleanupService = new StaticDeletionPlan();
return $oDataCleanupService->GetCleanupSummary($aRemovedClasses);
}
private function GetDeletionPlanSummaryTable(array $aDeletionPlanSummaryEntities): array
private function GetDeletionPlanSummaryTable(array $aRemovedClasses): array
{
$sName = 'DeletionPlanSummary';
$oDataCleanupService = new StaticDeletionPlan();
$aDeletionPlanSummaryEntities = $oDataCleanupService->GetCleanupSummary($aRemovedClasses);
$aColumns = ['Class', 'Delete Count' , 'Update Count', 'Issue Count'];
$aRows = [];
$iQueryCount = 0;
@@ -337,10 +302,9 @@ class DataFeatureRemovalController extends Controller
{
$this->ValidateTransactionId();
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary'));
Session::Unset('aDeletionExecutionSummary');
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
$aClasses = $oParameters->GetParameter('classes', []);
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$oDataCleanupService = new DataCleanupService();
$aDeletionExecutionSummary = $oDataCleanupService->ExecuteCleanup($aClasses);
@@ -355,7 +319,6 @@ class DataFeatureRemovalController extends Controller
$oSummary->iTotalUpdateCount += $oExecutionSummary->iUpdateCount;
}
Session::Set('aDeletionExecutionSummary', serialize($this->aDeletionExecutionSummary));
$this->OperationAnalysisResult();
}
@@ -363,9 +326,9 @@ class DataFeatureRemovalController extends Controller
{
$aExtensionsData = [];
if ($bIncludePackageExtensions) {
$aExtensionsRef = Metamodel::GetServiceLocator()->Get('DataFeatureRemoverExtension')->GetExtensionMap()->GetAllExtensionsWithPreviouslyInstalled();
$aExtensionsRef = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetAllExtensionsWithPreviouslyInstalled();
} else {
$aExtensionsRef = Metamodel::GetServiceLocator()->Get('DataFeatureRemoverExtension')->ReadItopExtensions();
$aExtensionsRef = DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions();
}
foreach ($aExtensionsRef as $oExtension) {
@@ -381,7 +344,7 @@ class DataFeatureRemovalController extends Controller
'uninstallable' => $oExtension->CanBeUninstalled(),
'remote' => $oExtension->IsRemote(),
'missing' => $oExtension->bRemovedFromDisk,
'dependency_issue' => $oExtension->HasDependencyIssue(),
'cannot-be-installed' => (!$oExtension->bInstalled && count($oExtension->aMissingDependencies) > 0),
],
];
@@ -449,11 +412,12 @@ class DataFeatureRemovalController extends Controller
/**
* Read extensions selected from posted parameters
* @return int Number of extensions to be added or removed
*/
public function ReadExtensionsDiff(): void
public function ReadExtensionsDiff(): int
{
if (!is_null($this->aExtensionsToCheck)) {
return;
return count($this->aExtensionsToCheck['to_be_installed']) + count($this->aExtensionsToCheck['to_be_removed']);
}
$aAvailableExtensions = $this->GetAvailableExtensions();
@@ -461,30 +425,26 @@ class DataFeatureRemovalController extends Controller
$this->aExtensionsToCheck = [
'to_be_installed' => [],
'to_be_removed' => [],
'extensions_not_uninstallable' => [],
];
foreach ($aAvailableExtensions as $sCode => &$aExtensionData) {
$sCodeFromUI = $sCode !== '' ? $sCode : iTopExtension::CODE_NO_CODE;
if (!isset($aSelectedExtensionsFromUI[$sCodeFromUI])) {
if (!isset($aSelectedExtensionsFromUI[$sCode])) {
continue;
}
if ($aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCodeFromUI] !== 'on') {
if ($aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] !== 'on') {
$aExtensionData['extra_flags']['selected'] = false;
$sLabel = $aExtensionData['label'];
$sLabel = $aAvailableExtensions[$sCode]['label'];
$this->aExtensionsToCheck['to_be_removed'][$sCode] = $sLabel;
if (! $this->bForcedUninstallation && $aExtensionData['extra_flags']['uninstallable']) {
$this->bForcedUninstallation = true;
}
if (false === $aExtensionData['extra_flags']['uninstallable'] || true === $aExtensionData['extra_flags']['remote']) {
$this->aExtensionsToCheck['extensions_not_uninstallable'][] = $sCode;
}
} elseif (!$aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCodeFromUI] === 'on') {
} elseif (!$aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] === 'on') {
$aExtensionData['extra_flags']['selected'] = true;
$sLabel = $aExtensionData['label'];
$sLabel = $aAvailableExtensions[$sCode]['label'];
$this->aExtensionsToCheck['to_be_installed'][$sCode] = $sLabel;
}
}
return count($this->aExtensionsToCheck['to_be_installed']) + count($this->aExtensionsToCheck['to_be_removed']);
}
/**

View File

@@ -13,11 +13,16 @@ class DeletionPlanEntity
public readonly DeletionPlanItem $oUpdate;
public readonly DeletionPlanItem $oIssue;
/**
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oDelete
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oUpdate
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oIssue
*/
public function __construct()
{
$this->oDelete = new DeletionPlanItem();
$this->oUpdate = new DeletionPlanItem();
$this->oIssue = new DeletionPlanItem();
$this->oDelete = $oDelete ?? new DeletionPlanItem();
$this->oUpdate = $oUpdate ?? new DeletionPlanItem();
$this->oIssue = $oIssue ?? new DeletionPlanItem();
}
public function TotalCount(): int

View File

@@ -0,0 +1,52 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Helper;
use MetaModel;
use utils;
class DataFeatureRemovalConfig
{
private static DataFeatureRemovalConfig $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): DataFeatureRemovalConfig
{
if (!isset(self::$oInstance)) {
self::$oInstance = new DataFeatureRemovalConfig();
}
return self::$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);
}
}

View File

@@ -1,34 +0,0 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Hook;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Service\Events\iEventServiceSetup;
use MetaModel;
use utils;
class EventListener implements iEventServiceSetup
{
/**
* @inheritDoc
*/
public function RegisterEventsAndListeners()
{
EventService::RegisterListener(
sEvent: \EVENT_SERVICE_LOCATOR_INITIALIZED,
callback: [$this, 'OnServiceLocatorInitialized']
);
}
public function OnServiceLocatorInitialized(EventData $oEventData): void
{
MetaModel::GetServiceLocator()->AddClassesFromFile(utils::GetAbsoluteModulePath(DataFeatureRemovalHelper::MODULE_NAME).'Config/ServiceLocator.xml');
}
}

View File

@@ -134,6 +134,10 @@ class DataCleanupService
/** @var DBObject $oDependentObj */
while ($oDependentObj = $oSet->Fetch()) {
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
if ($iDeletePropagationOption == DEL_MANUAL) {
$this->oObjectService->SetIssue(get_class($oDependentObj));
continue;
}
if ($oExtKeyAttDef->IsNullAllowed()) {
// Optional external key, list to reset
@@ -148,12 +152,6 @@ class DataCleanupService
return false;
}
} else {
// Mandatory external key
if ($iDeletePropagationOption == DEL_MANUAL) {
// Cannot be deleted automatically, must be handled manually
$this->oObjectService->SetIssue(get_class($oDependentObj));
continue;
}
// Propagate deletion only if not visited
if ($this->IsVisited($oDependentObj)) {
continue;

View File

@@ -13,19 +13,34 @@ use MetaModel;
class DataFeatureRemoverExtensionService
{
private static DataFeatureRemoverExtensionService $oInstance;
private ?iTopExtensionsMap $oMap = null;
private array $aItopExtensions = [];
private array $aIncludingExtensionsByModuleName = [];
public function __construct()
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 \Exception
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
*/
public function GetIncludingExtensions(string $sModuleName): array
{
@@ -47,7 +62,6 @@ class DataFeatureRemoverExtensionService
/**
* @return \iTopExtensionsMap
* @throws \Exception
*/
public function GetExtensionMap(): iTopExtensionsMap
{
@@ -60,7 +74,6 @@ class DataFeatureRemoverExtensionService
/**
* @return iTopExtension[]
* @throws \Exception
*/
public function ReadItopExtensions(): array
{

View File

@@ -59,11 +59,9 @@ class StaticDeletionPlan
{
foreach ($aClasses as $sClass) {
$oDeletionPlanItem = $this->GetInitialClassDeletionPlan($sClass);
// N°9831 Do not overwrite existing entity as it may already exist for this class if a previously processed class references it (issues/updates already accumulated must be kept)
if (false === array_key_exists($sClass, $this->aDeletionPlan)) {
$this->aDeletionPlan[$sClass] = new DeletionPlanEntity();
}
$this->aDeletionPlan[$sClass]->oDelete->Merge($oDeletionPlanItem);
$oDeletionPlanEntity = new DeletionPlanEntity();
$oDeletionPlanEntity->oDelete->Merge($oDeletionPlanItem);
$this->aDeletionPlan[$sClass] = $oDeletionPlanEntity;
$this->DeletionPlanForReferencingClasses($sClass);
}

View File

@@ -1,34 +0,0 @@
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% if bDeletionNeeded %}
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:DeletionPlan:Title'|dict_s} %}
{% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %}
{% EndUIFieldSet %}
{% if bDeletionPossible %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% else %}
{% UIAlert ForFailure { sTitle: '', sContent: 'DataFeatureRemoval:DeletionPlan:Error:Issues'|dict_s } %}{% EndUIAlert %}
{% endif %}
{% else %}
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:CleanupComplete:Title'|dict_s, sContent:'DataFeatureRemoval:CompilComplete'|dict_s } %}{% EndUIAlert %}
{% UIForm Standard {sId:'launch-setup-form', Action:sLaunchSetupUrl, EncType: 'application/x-www-form-urlencoded'} %}
{% for sKey, sValue in aSetupParams %}
{% UIInput ForHidden { sName:sKey, sValue:sValue } %}
{% endfor %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:true} %}
{% EndUIForm %}
{% endif %}
{% if bHasDeletionExecution %}
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Execution:Title'|dict_s} %}
{% UIDataTable ForForm { sRef:'aDeletionExecutionSummary', aColumns:aDeletionExecutionSummary.Columns, aData:aDeletionExecutionSummary.Data} %}{% EndUIDataTable %}
{% EndUIFieldSet %}
{% endif %}

View File

@@ -1,49 +1,91 @@
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:Analysis:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Analysis:Subtitle'|dict_s} %}
{% 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 %}
{% UIAlert ForInformation { sTitle: '', sId: 'ajax_compile_in_progress_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}
{{ 'DataFeatureRemoval:Compile:InProgress'|dict_s }}
{% UISpinner Standard { } %}
{% EndUIAlert %}
{% UIAlert ForSuccess { sTitle:'success', sId: 'ajax_compile_success_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
{% UIAlert ForFailure { sTitle:'error', sId: 'ajax_compile_error_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
{% UIAlert ForInformation { sTitle: '', sId: 'ajax_run_audit_in_progress_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}
{{ 'DataFeatureRemoval:RunAudit:InProgress'|dict_s }}
{% UISpinner Standard { } %}
{% EndUIAlert %}
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:RunAudit:Success'|dict_s, sId: 'ajax_run_audit_success_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
{% UIAlert ForFailure { sTitle:'error', sId: 'ajax_run_audit_error_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
{% if aAvailableExtensionsCount == 0 %}
{% UITitle Neutral { sTitle:'DataFeatureRemoval:NoResult:Title'|dict_s, iLevel:2 } %}{% EndUITitle %}
{% else %}
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Result:Title'|dict_s, sSubTitle: '' } %}
{% UIMultiColumn Standard {} %}
{% for iColumnIndex in 0..iColumnCount-1 %}
{% UIColumn Standard {} %}
{% for aExtension in aAvailableExtensions[iColumnIndex] %}
{% if aExtension['installed'] %}
{% UIExtensionDetails Installed { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source'], aExtension['code']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% UIExtensionDetails Installed { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% else %}
{% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source'], aExtension['code']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% endif %}
{% endfor %}
{% EndUIColumn %}
{% endfor %}
{% EndUIMultiColumn %}
{% EndUIPanel %}
{% endif %}
{% else %}
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Result:Title'|dict_s, sSubTitle: '' } %}
{% UIMultiColumn Standard {} %}
{% for iColumnIndex in 0..iColumnCount-1 %}
{% UIColumn Standard {} %}
{% for aExtension in aAvailableExtensions[iColumnIndex] %}
{% if aExtension['installed'] %}
{% UIExtensionDetails Installed { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% else %}
{% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% endif %}
{% endfor %}
{% EndUIColumn %}
{% endfor %}
{% EndUIMultiColumn %}
{% EndUIPanel %}
<div id="ajax_run_audit" class="ibo-block"></div>
{% if bDeletionNeeded %}
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:DeletionPlan:Title'|dict_s} %}
{% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %}
{% EndUIFieldSet %}
{% if bDeletionPossible %}
{% 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 sCode, sLabel in aAddedExtensions %}
{% UIInput ForHidden { sName:"aAddedExtensions[" ~ sCode ~ "]", sValue:sLabel } %}
{% endfor %}
{% for sCode, sLabel in aRemovedExtensions %}
{% UIInput ForHidden { sName:"aRemovedExtensions[" ~ sCode ~ "]", sValue:sLabel } %}
{% endfor %}
{% for sInputName, sValue in aHiddenInputs %}
{% UIInput ForHidden { sName:sInputName, sValue:sValue } %}
{% endfor %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% else %}
{% UIAlert ForFailure { sContent: 'DataFeatureRemoval:DeletionPlan:Error:Issues'|dict_s } %}{% EndUIAlert %}
{% endif %}
{% else %}
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:CleanupComplete:Title'|dict_s, sContent:'DataFeatureRemoval:CompilComplete'|dict_s, sId:value } %}{% EndUIAlert %}
{% UIForm Standard {'sId':'launch-setup-form', Action:sLaunchSetupUrl, 'EncType': 'application/x-www-form-urlencoded'} %}
{% for sKey, sValue in aSetupParams %}
{% UIInput ForHidden { sName:sKey, sValue:sValue } %}
{% endfor %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:true} %}
{% EndUIForm %}
{% endif %}
{% if bHasDeletionExecution %}
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Execution:Title'|dict_s} %}
{% UIDataTable ForForm { sRef:'aDeletionExecutionSummary', aColumns:aDeletionExecutionSummary.Columns, aData:aDeletionExecutionSummary.Data} %}{% EndUIDataTable %}
{% EndUIFieldSet %}
{% endif %}
{% endif %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}

View File

@@ -1,56 +0,0 @@
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
function ajax_run_audit(sTransactionId) {
$('#ajax_run_audit_in_progress_msg').removeClass('ibo-is-hidden');
$.post(
'{{ sAjaxURL|raw }}',
{ operation: 'ajax_run_audit', transaction_id: sTransactionId },
function (data) {
$('#ajax_run_audit_in_progress_msg').addClass('ibo-is-hidden');
if (data.error_message) {
$('#ajax_run_audit_error_msg .ibo-alert--title').html(data.error_message);
$('#ajax_run_audit_error_msg').removeClass('ibo-is-hidden');
} else {
$('#ajax_run_audit_success_msg').removeClass('ibo-is-hidden');
$('#ajax_run_audit').html(data);
}
}
)
.fail(function() {
$('#ajax_run_audit_in_progress_msg').addClass('ibo-is-hidden');
$('#ajax_run_audit_error_msg .ibo-alert--title').html('{{ 'DataFeatureRemoval:RunAudit:Error'|dict_s }}');
$('#ajax_run_audit_error_msg').removeClass('ibo-is-hidden');
});
}
function ajax_compile() {
$('#ajax_compile_in_progress_msg').removeClass('ibo-is-hidden');
$.post(
'{{ sAjaxURL|raw }}',
{ operation: 'ajax_compile', transaction_id: '{{ sTransactionId }}' },
function (data) {
$('#ajax_compile_in_progress_msg').addClass('ibo-is-hidden');
if (data.error_message) {
$('#ajax_compile_error_msg .ibo-alert--title').html(data.error_message);
$('#ajax_compile_error_msg').removeClass('ibo-is-hidden');
} else {
$('#ajax_compile_success_msg .ibo-alert--title').html(data.success_message);
$('#ajax_compile_success_msg').removeClass('ibo-is-hidden');
ajax_run_audit(data.transaction_id);
}
},
'json'
)
.fail(function() {
$('#ajax_compile_in_progress_msg').addClass('ibo-is-hidden');
$('#ajax_compile_error_msg .ibo-alert--title').html('{{ 'DataFeatureRemoval:Compile:Error'|dict_s }}');
$('#ajax_compile_error_msg').removeClass('ibo-is-hidden');
});
}
ajax_compile();

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