Compare commits

..

234 Commits

Author SHA1 Message Date
Eric Espie
8e0ae67803 N°4227 / N°4225 - Enhance SetupUtils interface 2021-09-06 17:53:46 +02:00
odain
095c975ec6 N°4227 / N°4225 - require cleanup + fix manual restore 2021-08-12 16:25:50 +02:00
odain
9e1e5a8a47 N°4227 - Manual iTop restore - protect any restore from cron or db updates and release readonly mode afterward only if restore set it itself 2021-08-12 16:25:50 +02:00
odain
2c026fa891 N°4225 - Protect manual backups from cron (and vice versa) - protect any backup from cron/db updates and release readonly mode only when backup set it itself 2021-08-12 16:25:50 +02:00
odain
00d65aeb45 N°4227 - Manual iTop restore - create manual CLI restore 2021-08-12 16:25:50 +02:00
odain
5702f603ac N°4227 - Manual iTop restore - move restore mutex management inside in DBRestore->RestoreFromCompressedBackup primitive 2021-08-12 16:25:50 +02:00
odain
e3dc1b77cc N°4225 - Protect manual backups from cron (and vice versa) - minimal work ie protect manual backup from cron only 2021-08-12 16:25:50 +02:00
Stephen Abello
c304f70ff4 Harmonize external key's autocomplete items display with dropdown ones 2021-08-12 14:40:36 +02:00
Stephen Abello
53fd41e748 N°4127 Fix XSS vulnerability in autocomplete lists 2021-08-12 14:21:05 +02:00
Molkobain
7577fbb8bf N°4236 - Fix user with no admin right can't connect in iTop when log_usage set to true 2021-08-12 11:13:59 +02:00
Molkobain
0fc912b357 Rename MetaModel::IsObjectExistsInDb() to MetaModel::IsObjectInDB() 2021-08-12 09:37:24 +02:00
Molkobain
c475e66176 Add documentation 2021-08-12 09:35:14 +02:00
Pierre Goiffon
cd1ba097cb N°3867 Fix error "LEVEL_WARNING of type Identifier is forbidden" error when saving valid config in configuration editor 2021-08-11 18:07:38 +02:00
Molkobain
fdca4d4cc3 N°3786 - Fix object locked hint (on activity panel) not displayed until message removed 2021-08-11 17:41:31 +02:00
Pierre Goiffon
4379b4d908 N°3800 Fix HTML content display for table borders v2 2021-08-11 17:31:08 +02:00
Pierre Goiffon
5b42f67a99 N°3867 Code review: remove condition that is quite too strict (danger !!)
Thanks @odain !
2021-08-11 17:22:21 +02:00
Pierre Goiffon
0b9ccc8e67 N°3800 Fix HTML content display for table borders and code color 2021-08-11 17:08:00 +02:00
Pierre Goiffon
2d98ca2318 N°3867 Fix "Invalid configuration: Stmt_Expression is forbidden in line 10" error when saving valid config in configuration editor
Now this is tested in \ConfigValidator\iTopConfigAstValidatorTest
2021-08-11 16:38:56 +02:00
odain
92add2bbfe add annotation to datasynchro test 2021-08-11 10:17:35 +02:00
Pierre Goiffon
f15bdb75ca N°4173 Fix memory_limit error on target object hyperlink generation 2021-08-10 19:08:00 +02:00
Pierre Goiffon
a66830de17 N°4173 Fix memory_limit error when saving an object pointing to a big AttributeBlob
Check of pointed object was loading the whole object instead of just doing a count
2021-08-10 19:08:00 +02:00
Pierre Goiffon
714294e1b4 N°4173 Fix memory_limit exhausted when having ExtKey widget pointing on class with AttributeBlob
Was failing when opening an object form, and when the list contained target objects with total file size above memory_limit
2021-08-10 18:46:10 +02:00
Molkobain
e3d2c1d761 Advanced search: Restore "more criteria" inputs color 2021-08-10 16:27:08 +02:00
Molkobain
59d95cc14b Field: Change how field's value wrap to fix unwanted "new line" between some elements (lists, ...) 2021-08-10 16:27:08 +02:00
Pierre Goiffon
ddc5bbd1bb N°3867 Fix write config now removes PHP vars
Now the PhpParser tree has a different structure, beginning with a PhpParser\Node\Stmt\Expression
2021-08-10 15:47:01 +02:00
Pierre Goiffon
9bbee0d603 ⬆️ N°3867 Upgrade php-parser from 3.1.5 to latest version 4.12.0
The 3.1.5 version wasn't maintained anymore and was compatible with PHP 7.2 maximum
2021-08-10 15:45:02 +02:00
acognet
be1ef5b452 N°3634 - Feedback alpha 3.0 : finish list - display printable object 2021-08-10 12:10:54 +02:00
Pierre Goiffon
cbdc48b7e1 iTopConfigParserTest now works on Windows 2021-08-10 11:48:13 +02:00
denis.flaven@combodo.com
bb5679959e Fixed unit tests for password_renewed_date, broken by the correction of
n°4095.
2021-08-10 10:40:54 +02:00
Molkobain
cdbb4470fc N°4039 - Add margin below an object details
- Easier read of the fields at the bottom of it
- Better display of a dropdown at the bottom of it
2021-08-09 17:20:56 +02:00
Molkobain
252d752bf5 Navigation menu: Improve french translations for the current user tooltip 2021-08-09 17:20:56 +02:00
denis.flaven@combodo.com
99d0c05c1c N°4095 - Provisions for future implementation of users password
expiration.
2021-08-09 17:13:08 +02:00
Molkobain
5926e9d110 Navigation menu: Add current user's login to the tooltip 2021-08-09 16:28:53 +02:00
Molkobain
c8dd8c3806 Code cleanup: Remove forgotten commented line 2021-08-09 13:54:02 +02:00
Molkobain
70b07721e6 N°2875 - Fix typo in config example 2021-08-09 10:46:42 +02:00
Molkobain
bd050dfe69 N°2875 - Add possibility to configure the marker scope by either a class or an OQL 2021-08-09 10:38:27 +02:00
Molkobain
9eb477ce83 N°2875 - Fix visual glitches on long results
- Medallion could be horizontally compressed
- Title wrapped instead of overflowing, making reading across entries difficult
2021-08-09 10:38:27 +02:00
Pierre Goiffon
a742b6c610 🔊 N°3002 DeprecatedCallsLog error handler : add trigger_error message 2021-08-09 09:06:17 +02:00
Pierre Goiffon
fcf769666e N°3913 setup : in "done" step, add back styling on HTML content 2021-08-06 17:59:42 +02:00
Pierre Goiffon
72e628c5d5 💡 Reference in PHPDoc for \LogAPI::GetMinLogLevel 2021-08-06 17:39:34 +02:00
Pierre Goiffon
bf28602ae6 💡 Improve PHPDoc for \LogAPI::GetMinLogLevel 2021-08-06 17:37:56 +02:00
Pierre Goiffon
b8a0d899f4 Fix \coreExtensions\UserLocalTest::testValidatePassword failing because of DEPRECATED notices 2021-08-06 17:13:24 +02:00
Pierre Goiffon
d335736cfc 🔊 N°3002 Add trigger_error to catch deprecated notices inside DeprecatedCallsLog
They are logged as WARNING level in \DeprecatedCallsLog::ENUM_CHANNEL_PHP_LIBMETHOD channel

Such deprecated notices are generated inside Symfony for example using @trigger_error(..., E_DEPRECATED).
Having such a log will help us migrate to the next lib version !
2021-08-06 17:13:24 +02:00
Molkobain
80f7d07378 N°3592 - Fix alignment of custom fields in the backoffice 2021-08-06 17:04:26 +02:00
Molkobain
0be6a8aef4 N°3592 - Remove part of 146a95bae as it was unnecessary 2021-08-06 17:04:26 +02:00
Stephen Abello
0fe857071d Fix login css glitches with iTop 3.0 2021-08-06 15:41:43 +02:00
acognet
05981c8af8 N°4156 - GUI broken when a external key contains "\" 2021-08-06 11:56:25 +02:00
Molkobain
0abea749fa N°3786 - Fix lock removal when leaving an object details page in read-only with lock on the activity panel 2021-08-06 11:12:56 +02:00
Molkobain
78af6fdc84 N°3944 - Add comment to explain the 2 similar IFs 2021-08-06 11:11:57 +02:00
acognet
1e97b5a8c0 Revert N°3423 - Allow AttributeImage / AttributeDocument content to be cached by the browser + N°4029 - Caching images in chrome does not work 2021-08-06 09:51:59 +02:00
acognet
0214243b63 N°3914 - Polishing: Lists - remove pagination when it is unnecessary 2021-08-06 09:19:31 +02:00
Pierre Goiffon
d30871ac59 Revert "N°3002 Add trigger_error to catch deprecated notices inside DeprecatedCallsLog"
This reverts commit d247ea915d.
Will solve CI build errors... I will work in my own branch to tweak this !
2021-08-05 17:35:17 +02:00
Pierre Goiffon
d247ea915d N°3002 Add trigger_error to catch deprecated notices inside DeprecatedCallsLog
Such notices are generated inside Symfony for example using @trigger_error(..., E_DEPRECATED).
Having such a log will help us migrate to the next version !
2021-08-05 17:18:19 +02:00
acognet
0e0aed1ba4 N°3423 - Allow AttributeImage / AttributeDocument content to be cached by the browser + N°4029 - Caching images in chrome does not work 2021-08-05 16:34:24 +02:00
acognet
bc770ef3d5 N°4007 - When a search is on auto submit, auto submit it also on first display - new fix in order to avoid reload in case of OQLMenuNode -add param in searchFormHandler 2021-08-05 16:20:56 +02:00
Pierre Goiffon
56a4fb0b42 Allow to run all tests from within the IDE
Just :
* configure the config file (test\phpunit.xml.dist)
* add runner parameters : --fail-on-risky --exclude-group beforeSetup
* specify iTop root as custom working directory
2021-08-05 15:21:41 +02:00
acognet
3139628dd8 N°4007 - When a search is on auto submit, auto submit it also on first display - new fix in order to avoid reload in case of OQLMenuNode 2021-08-05 15:08:48 +02:00
acognet
7d9b19cd9e N°4119 - Loose application context in advanced search ajax call 2021-08-05 12:15:47 +02:00
Stephen Abello
b3cb95d2f1 Removed inline background color 2021-08-05 11:56:17 +02:00
Stephen Abello
a6765cdc3a Update precompiled stylesheets 2021-08-05 10:38:40 +02:00
Stephen Abello
97d52fb539 N°3685 Revert most of e4f58b5b changes, replace @extend .ibo-button calls with classes in markup for most cases, except jquery-ui buttons 2021-08-05 10:20:30 +02:00
Pierre Goiffon
ad936d7a2f 🔇 N°3731 Remove DeprecatedCallLog in \MetaModel::BulkUpdate
Method is still called in \ObsolescenceDateUpdater !
2021-08-04 17:37:10 +02:00
Pierre Goiffon
090eb9a560 🔇 N°3731 Remove DeprecatedCallLog in \MetaModel::GetFilterCodeOrigin
Method is still called !
2021-08-04 17:34:13 +02:00
Stephen Abello
e4f58b5b98 N°3685 Greatly optimize SCSS rules to reduce compiled CSS file size (4.7Mo -> 620ko) 2021-08-04 16:38:40 +02:00
acognet
c99a22c9f7 N°4025 - Universal search : cannot select class anymore 2021-08-04 14:41:25 +02:00
Pierre Goiffon
29cf20beaf N°3909 Fix notification configuration help layout 2021-08-04 12:00:48 +02:00
denis.flaven@combodo.com
e325d535ae N°4096 - Add automatic email reprocessing in case of error 2021-08-04 11:24:59 +02:00
Pierre Goiffon
11cfdb2a17 N°3807 Fix container added for all Html components
This was causing issues for example in the setup, as any raw content is sent using \WebPage::add, which create an HTML component.
I reverted the layout update, and created a new factory method to be used when needed : \Combodo\iTop\Application\UI\Base\Component\Html\HtmlFactory::MakeHtmlContent
2021-08-04 11:19:35 +02:00
Pierre Goiffon
66720b9731 N°3807 Update theme CSS 2021-08-04 11:19:35 +02:00
acognet
234f46cafa N°1731 - Allow Transitions without unnecessary confirmation 2021-08-04 10:51:04 +02:00
acognet
27c3ce0389 N°3907 - Polishing: Run query - Add dictionary entry 2021-08-04 10:44:26 +02:00
Pierre Goiffon
9d0e2fa64a N°3807 Bring back styling to raw HTML content
Bulma minireset (https://github.com/jgthms/bulma/blob/master/sass/base/minireset.sass) was applied everywhere in iTop. This was causing HTML content without any Bulma or iTop 3.0.* CSS classes to render with no styles anymore, not even the default browser's ones. Especially rendering for content styled in CK Editor was problematic...

This commit creates a new `ibo-is-html-content` CSS class (in css/backoffice/utils/helpers/_misc.scss) that just extends the `content` Bulma class (indirection to reduce framework coupling).
This new iTop CSS class is added in :
* AttributeText and its children when format is HTML
* HTML components
* activity entries in HTML format

The class can also be used elsewhere when needed, for example in modules having custom pages that aren't using yet the iTop 3.0.* UI components or CSS classes.
2021-08-04 10:37:54 +02:00
acognet
e211633fed N°4039 - List contain stays in place when scrolling on a screen 2021-08-04 10:37:02 +02:00
denis.flaven@combodo.com
29967aa41a N°3944 - LogAPI : fix default log level in config when logging using a channel 2021-08-04 10:07:58 +02:00
Pierre Goiffon
3ebb1cfc66 💡 phpDoc 2021-08-03 17:15:38 +02:00
Molkobain
6c2221b8b6 Datatables: Remove extra white space in template 2021-08-03 16:16:21 +02:00
Molkobain
c0b3aed12c N°3712 - Code cleanup thanks to @piR 2021-08-03 16:15:09 +02:00
Stephen Abello
53efc9f75e Update precompiled stylesheets 2021-08-03 15:55:06 +02:00
Stephen Abello
0d566f2e47 Fix panel collapsible toggler display 2021-08-03 15:49:27 +02:00
Stephen Abello
b294e3c734 N°3928 Correctly align impact analysis's filter refresh button 2021-08-03 15:49:27 +02:00
Stephen Abello
6704f9eccf N°3928 Allow user to collapse impact analysis' filter 2021-08-03 15:49:27 +02:00
Molkobain
90cb6e1d49 N°4124 - Extract computations into variables for easier comprehension 2021-08-03 11:37:01 +02:00
Molkobain
c0aa4f2d69 Initialize array properly 2021-08-03 11:11:02 +02:00
Molkobain
01984c24c0 Fix wrong usage of spaceless filter 2021-08-02 20:15:16 +02:00
Molkobain
1da1e0b1bd N°3712 - Activity panel: Improve history entries to have a different icon depending the origin (interactive, webservices, csv, ...) 2021-08-02 19:42:32 +02:00
Molkobain
39bcd3e4cd 🌐 N°3712 - Add dict. entries for CMDBChange->origin values 2021-08-02 19:42:32 +02:00
Molkobain
156cce6098 N°3712 - Extract CMDBChange origin values in a dedicated class 2021-08-02 19:42:32 +02:00
acognet
3130e95f4f N°3907 - Polishing: Run query 2021-08-02 18:56:07 +02:00
Molkobain
e28f704f3e N°4076 - Revert regression in global search from 344cce9fd
Current query was not visible when reaching the results page
2021-08-02 15:20:05 +02:00
Stephen Abello
5c6c59941a N°3901 Correctly hide menu search placeholder 2021-08-02 15:18:06 +02:00
Stephen Abello
bc9d47933e Update precompiled stylesheets 2021-08-02 11:43:28 +02:00
Stephen Abello
07a10e4146 N°3930 Fix dashlet blocker position 2021-08-02 11:42:57 +02:00
Stephen Abello
e8a21870ad Update precompiled stylesheets 2021-08-02 11:28:37 +02:00
Stephen Abello
2e132d5c53 N°3930 Restore dashlet blocker on dashboard editor 2021-08-02 11:28:37 +02:00
Eric
3359609349 Allow mode selection for queries 2021-07-31 15:23:19 +02:00
Molkobain
2966759466 Code cleanup 2021-07-30 17:09:51 +02:00
Molkobain
25395405e5 N°3905 - Remove remaining spaces 2021-07-30 17:09:50 +02:00
Stephen Abello
b589e2d001 Update precompiled stylesheets 2021-07-30 16:48:48 +02:00
Stephen Abello
27f3619cf5 Fix undefined variable used in scss 2021-07-30 16:48:23 +02:00
Stephen Abello
616fc436d0 CSS improvements 2021-07-30 16:40:33 +02:00
acognet
4f2f765207 N°3912 - Polishing: Export - fix page webservices/export.php - revert delete of utils.js 2021-07-30 15:16:51 +02:00
Pierre Goiffon
9931fa1a6b 🎨 N°4092 rename method
Thanks @Molkobain !
2021-07-30 12:09:30 +02:00
Pierre Goiffon
a13f2750ea 🎨 Replace call_user_func call to \cmdbAbstractObject::GetShortcutActions for better IDE recognition
And also :
* removed an obsolete use statement
* fix method phpdoc
2021-07-30 11:41:41 +02:00
Stephen Abello
243d105f59 Fix return value from new method added in 49fd482 2021-07-30 10:54:16 +02:00
Stephen Abello
7783ba570e PHPDoc 2021-07-30 10:37:52 +02:00
acognet
5a9fa2ac32 N°3912 - Polishing: Export - fix page webservices/export.php 2021-07-29 17:59:51 +02:00
Pierre Goiffon
619e3de5a8 N°4092 Setup : add symlinks option in install
Was only available during update
2021-07-29 17:34:24 +02:00
Pierre Goiffon
83064d68c7 N°4092 Setup : symlink option now always displayed if functionality enabled
Previously we were also testing for flag presence.
In consequence the confirmation dialog when unchecking the option is also removed (we can enable back the option using the setup)
2021-07-29 17:24:20 +02:00
Stephen Abello
f3c11e72cf N°3552 Add method to retrieve User object from Person object 2021-07-29 16:37:03 +02:00
Stephen Abello
c9e887a264 N°3552 Allow AddLogEntry to set entry's user_id from another user using a new parameter 2021-07-29 16:37:02 +02:00
Stephen Abello
49fd482389 N°3552 Correctly display case log entry with no iTop user linked (such as mail to ticket entries) 2021-07-29 16:37:02 +02:00
acognet
96de4e1796 N°4117 - Text field display, too many empty lines 2021-07-29 11:52:39 +02:00
acognet
44f413583c N°4124 - Unable to scroll down on some object 2021-07-29 11:38:20 +02:00
acognet
91104002ea N°4037 - Missing close button on list export on portal - rollback commit for N°3746 2021-07-28 16:02:35 +02:00
acognet
f98ba1594c N°3705 - Migrate module to new UIBlock system : Kanban board - css and dictionnary in ajax page 2021-07-28 16:02:35 +02:00
Molkobain
431dc5532b Change comment / test for clarification 2021-07-28 15:59:43 +02:00
Molkobain
df473ae313 PHPDoc 2021-07-28 15:31:22 +02:00
Molkobain
40ce74cffa N°4203 - Activity panel: Add button to load all entries at once 2021-07-28 14:58:15 +02:00
Molkobain
7598c18ad6 N°4203 - Activity panel: Hide "load more entries" button when only "logs" filter is active 2021-07-28 14:55:17 +02:00
Molkobain
d38b655f5f Activity panel: Limit visual glitches on entries on first display by hiding them all before displaying only the relevant.
Otherwise, entries from the other logs / activity tabs are visible for a second before filtering on the entry of the current tab only.
2021-07-28 14:55:17 +02:00
Eric
f4345ef312 N°4199 - Add <code> to EnumSet values to fix XML conversion 2021-07-28 14:40:19 +02:00
Eric
13b548e95d N°4199 - Add <code> to EnumSet values to fix XML conversion 2021-07-28 14:39:52 +02:00
Eric
3a988ab499 N°4175 - Adding a lifecycle to an existing class fails on setup (using xml version < 3.0) 2021-07-28 13:30:39 +02:00
Eric
14a5f87d62 N°4036 - User edition controls : a user cannot remove contact 2021-07-28 13:09:18 +02:00
Eric
8dae459b12 N°4036 - User edition controls : the profiles selection should allow the User modification (when editing your own User) 2021-07-28 11:45:02 +02:00
Eric
8dc10424e8 🎨 cleanup code 2021-07-28 09:59:10 +02:00
Eric
54a6573948 N°4036 - User edition controls : the profiles selection should allow the User modification (when editing your own User) 2021-07-28 09:13:47 +02:00
Eric
1d5e0b6fe9 Temporary ignored test for CI 2021-07-27 18:01:39 +02:00
Eric
acb8a377dd N°4036 - User edition controls : a user cannot change his own status (fix) 2021-07-27 17:40:00 +02:00
odain
d86f489c89 fix provided themes to avoid scss setup compilation 2021-07-27 17:22:24 +02:00
Eric
2c154b601d N°4036 - User edition controls : allowed orgs must contain user org 2021-07-27 15:57:03 +02:00
Eric
7dad079688 N°4036 - User edition controls : a user cannot remove contact 2021-07-27 15:57:03 +02:00
Eric
ace2eb6dab N°4036 - User edition controls : a user cannot change his own status 2021-07-27 15:57:03 +02:00
Eric
25f3c1cbc4 N°4036 - User edition controls : a user cannot add to himself a profile denying the backoffice 2021-07-27 15:57:03 +02:00
Eric
8f7e7c136d Fix links when object edition fails 2021-07-27 15:57:03 +02:00
Eric
71a7e060e4 N°4036 - User edition controls : a user cannot delete himself 2021-07-27 15:57:03 +02:00
Pierre Goiffon
c450f1d02f 💡 aModifierProperties documentation 2021-07-27 15:54:47 +02:00
acognet
4431762c10 N°3592 - Migrate module to new UIBlock system : Customized request forms - precompiled css v2 2021-07-27 15:33:47 +02:00
acognet
97170892e6 N°3905 - Polishing: CSV Import - remove spaces 2021-07-27 15:08:38 +02:00
acognet
5137d634e3 N°3592 - Migrate module to new UIBlock system : Customized request forms - precompiled css 2021-07-27 15:03:04 +02:00
acognet
146a95baec N°3592 - Migrate module to new UIBlock system : Customized request forms - remove space in edit screen 2021-07-27 14:38:44 +02:00
Molkobain
a7ac9126a2 PHPDoc 2021-07-26 18:07:12 +02:00
Eric
ee544b646d N°4076 - PHPDoc 2021-07-26 17:48:36 +02:00
Eric
344cce9fdd N°4076 - Allow block parameters to change the behaviour of blocks on the page 2021-07-26 17:20:45 +02:00
Stephen Abello
bfcfcdd4cc N°3930 Fix dashlet group by preview not displaying 2021-07-26 14:26:44 +02:00
Molkobain
4410bf7e1f N°4144 - Portal: Fix caselog fullscreen icon too small to be clickable 2021-07-26 12:07:49 +02:00
Eric
d1fda1dbc6 composer classmap changed 2021-07-23 16:47:15 +02:00
Stephen Abello
5c702be641 Update precompiled themes 2021-07-23 11:29:33 +02:00
Stephen Abello
8a9ad78e9c N°3930 Bring back error icons on dashlet property 2021-07-23 11:29:33 +02:00
Molkobain
778be8abce N°4195 - Avoid dictionary entries to be added to the setup page as it breaks the fresh install
In fresh install there is no env-xxx yet, so when the setup tries to load the english dictionary from there it crashes.
In setup pages, all string are hard coded, no dictionary entries are available (yet)
2021-07-23 09:00:19 +02:00
Molkobain
0760adcef6 Object details: Fix status code being set to the color instead of the code 2021-07-22 18:12:11 +02:00
Pierre Goiffon
29ca291860 N°4192 Fix error on Contract classes object details
Was crashing on all classes without state attribute, or with state attribute nullable
2021-07-22 11:07:07 +02:00
Stephen Abello
93d0e4ae7c Update jQuery datepicker z-index 2021-07-22 10:43:22 +02:00
Molkobain
61bc07b598 Add PHPDoc to explain behavior change (as in the migration notes) 2021-07-21 17:46:39 +02:00
Pierre Goiffon
00ee36f938 Merge remote-tracking branch 'origin/support/2.7' into develop
# Conflicts:
#	README.md
2021-07-21 12:22:20 +02:00
Pierre Goiffon
1aa5185c93 Merge remote-tracking branch 'origin/support/2.6' into support/2.7
# Conflicts:
#	README.md
2021-07-21 12:19:52 +02:00
Pierre Goiffon
834ac00d37 📝 README : update latest releases
Was made in #143 but on develop only, but we are still maintaining older branches !
2021-07-21 12:15:22 +02:00
Molkobain
957cb87b8d N°3786 - Fix lock removal when leaving an object details page in edition
The main problem was WebPage::$a_scripts are now printed after the DOM when the mechanism actually prints some JS to redirect to another page immediately.
As there are a LOT of calls to WebPage::add_script(), we kept it to ensure the compatibility; and we add a new WebPage::add_early_script() for JS snippets that explicitly need to be execute before the DOM interpretation.
2021-07-20 20:13:44 +02:00
Eric
d0813f6607 N°4169 - Fix forms properties for designer 2021-07-20 17:49:30 +02:00
Stephen Abello
cf4673d284 Update precompiled stylesheets 2021-07-20 10:21:06 +02:00
Stephen Abello
e3422f5fd9 CSS improvements 2021-07-20 10:20:31 +02:00
Stephen Abello
d7a0878a39 Fix broken variables import in 3.0 themes 2021-07-20 10:04:12 +02:00
Molkobain
076f0e00a7 N°4178 - Stay on the same page when logging again from the "Login again" prompt 2021-07-18 22:58:33 +02:00
Molkobain
eb04ecb5b4 Fix new URL parameter being append after the '#' in CombodoGlobalToolbox.AddParameterToUrl 2021-07-18 22:37:01 +02:00
Molkobain
3323d5532b JSDoc 2021-07-18 22:04:39 +02:00
Molkobain
d68d8204f2 N°4176 - Portal: Deprecate "AddParameterToUrl" function 2021-07-18 22:02:47 +02:00
Molkobain
4a50b95069 N°3924 - Fix misleading orphan activity entries when creating an object 2021-07-17 00:11:16 +02:00
Molkobain
fd9e4f413c N°3924 - Fix missing activity panel on object details when cancelling transition form 2021-07-16 22:53:28 +02:00
Molkobain
dca3fc996c N°3924 - Fix double encoding of the object name in transition page title 2021-07-16 22:39:15 +02:00
Molkobain
c58c1bbf1d N°3924 - Fix notification link in activity panel not opening notifications tab 2021-07-16 22:29:54 +02:00
Molkobain
524919b946 N°4171 - Remove lock message from the log entry form when object is in edition 2021-07-16 20:13:49 +02:00
Molkobain
e15953524a N°3786 - Add pause at the end of the page unload async request to follow the guideline used in N°2763 2021-07-16 20:08:25 +02:00
Molkobain
68f3c53faa N°4171 - Concurrent lock: Fix expiration modal being duplicated sometimes 2021-07-16 16:44:35 +02:00
Molkobain
82e3ddaacd N°4142 - Improve backoffice rendering by showing system avatar no matter what 2021-07-15 21:34:58 +02:00
Molkobain
dfbbee2a1c N°4142 - Add config. param. to hide user avatars in the GUIs -mostly in logs- (backoffice, end-users portal, ...) 2021-07-15 21:23:20 +02:00
Molkobain
4843545171 N°4142 - Fix UserRights::GetUserPictureAbsUrl() returning user's placeholder picture for users with an unknown login 2021-07-15 17:33:38 +02:00
Molkobain
4e58b8616a N°3203 - PHPDoc and small variable refactor 2021-07-15 17:31:30 +02:00
Molkobain
1a79dcd773 N°3203 - Portal: Fix image attribute of an object not authorized if object out of scope 2021-07-15 11:41:07 +02:00
Molkobain
a4104d4315 N°3539 - Remove viewport meta tag as we won't be able to make the backoffice responsive in 3.0 2021-07-15 09:09:47 +02:00
Molkobain
4e1db7d7e2 Merge remote-tracking branch 'origin/support/2.7' into develop 2021-07-13 11:44:42 +02:00
Molkobain
8e6379a112 Merge branch 'support/2.7.5' into support/2.7 2021-07-13 11:42:30 +02:00
Molkobain
da217a1cb3 N°4161 - Fix ManageBrick crash when no item listed 2021-07-12 18:10:22 +02:00
Pierre Goiffon
a683634a05 N°4126 Fix HTML escaped in \SetupUtils::CheckDbServer messages
As content is sent to JS returned to the AJAX request, we need to escape JS string delimiter (single quote)
We had previously a \utils::HtmlEntities call, but this isn't necessary as all content is generated internally, without calling any dict or extensibility interface.
2021-07-12 14:41:26 +02:00
Molkobain
2f1b5d2736 Fix typo in method parameter 2021-07-12 12:47:43 +02:00
Molkobain
c9bf784fce Revert space introduced by 418312bca 2021-07-12 11:37:17 +02:00
Molkobain
4ef10ae7c9 PHPDoc 2021-07-12 10:58:38 +02:00
Molkobain
5174250ff1 N°3836 - Dashlet: Refactor part of the code to match conventions 2021-07-12 10:51:45 +02:00
Pierre Goiffon
7b6ac202c6 N°4518 Add config parameter to set platform as dev env (#220)
developer_mode.enabled, default value null
If true or false then will be used by \utils::IsDevelopmentEnvironment as return value, in all other cases will follow previous behavior
2021-07-12 09:27:36 +02:00
Molkobain
d960183403 Merge remote-tracking branch 'origin/support/3.0.0-beta2' into develop 2021-07-09 16:28:00 +02:00
Eric
ece3e0490d N°3573 - Allow retrieving operation
(cherry picked from commit e6a38a8055)
2021-07-09 15:24:20 +02:00
Eric
1562cb1f38 🎨 Allow tab stickiness for twig based extensions
(cherry picked from commit 1a7755365c)
2021-07-09 15:23:35 +02:00
Molkobain
11a22abfd5 SCSS: Fix horizontal scrollbars being ticker than vertical ones on webkit browsers 2021-07-09 12:52:32 +02:00
Stephen Abello
5254c9a633 N°3930 Fix dashlets select property with a set size 2021-07-08 10:32:41 +02:00
Stephen Abello
f7a35072f5 N°4075 Fix autocompletes in modals 2021-07-07 16:59:50 +02:00
Molkobain
b5f5780f35 Merge remote-tracking branch 'origin/support/3.0.0-beta2' into develop
# Conflicts:
#	datamodels/2.x/itop-structure/precompiled-themes/fullmoon/main.css
#	datamodels/2.x/itop-structure/precompiled-themes/test-red/main.css
2021-07-07 15:08:38 +02:00
Molkobain
f9064084f9 N°4130 - Activity panel: Remove edits and transitions from default filters on logs 2021-07-07 14:11:03 +02:00
Molkobain
67afbd1d8d N°4146 - Fix crash on object deletion 2021-07-07 13:18:10 +02:00
Molkobain
d6b9172e26 N°4131 - Fix multiple "User disconnected" dialogs in the backoffice
(cherry picked from commit d8f36a8aa9)
2021-07-07 09:32:01 +02:00
Molkobain
8e1e71c740 N°4109 - Fix JS message "Gateway timeout" / "Unauthorized" / "" being displayed on object details page
(cherry picked from commit e279799bf8)
2021-07-07 09:31:53 +02:00
Molkobain
ebbf6e56be N°4127 - Security: Fix XSS vulnerability in object attribute's tooltip 2021-07-07 09:28:41 +02:00
Pierre Goiffon
bd67b71f3d Merge remote-tracking branch 'origin/support/2.7' into develop 2021-07-06 11:36:34 +02:00
Pierre Goiffon
69ad10785b 🔧 .editorConfig : disable PHP variables alignement
Still enabled for key/value pairs though ! Example in \UtilsTest::ConvertToBytesProvider
2021-07-05 12:34:20 +02:00
Pierre Goiffon
9aead898e2 Fix Wiki URL to use iTop Hub instead of wiki.openitop
Thanks @Molkobain and @Hipska !
2021-07-05 12:29:07 +02:00
Pierre Goiffon
a48ebfefba N°4126 Change max_allowed_packet error message
Thanks @Molkobain !
2021-07-05 09:54:47 +02:00
Pierre Goiffon
e2a6e6b846 💚 Fix compiled CSS 2021-07-05 09:53:11 +02:00
Molkobain
7ca689e190 Setup: Fix sizes being displayed as bits instead of bytes 2021-07-04 22:49:54 +02:00
Molkobain
d8f36a8aa9 N°4131 - Fix multiple "User disconnected" dialogs in the backoffice 2021-07-04 21:54:56 +02:00
Molkobain
e279799bf8 N°4109 - Fix JS message "Gateway timeout" / "Unauthorized" / "" being displayed on object details page 2021-07-04 16:46:49 +02:00
Eric
a117906ff6 🎨 Add constant for cmdbsource log channel 2021-07-02 17:01:46 +02:00
Pierre Goiffon
c76d4f12fd Fix constant remove in iTopHub:DBBackupSentence ZN dict key
Thanks Hipska for your feedback in the original commit 67e92120 !
2021-07-02 16:30:31 +02:00
Molkobain
b16529e337 Update version.xml to 3.0.0-beta2 2021-07-02 12:28:58 +02:00
Molkobain
67e9212008 Massively update dictionaries 2021-07-02 12:26:51 +02:00
acognet
35b70bfc00 N°3929 - Polishing: Dashboards 2021-07-02 11:29:10 +02:00
acognet
418312bca5 N°3930 - Polishing: Dashlets 2021-07-02 11:22:57 +02:00
Pierre Goiffon
4748717e50 N°4126 Improve max_allowed_packet checks messages 2021-07-02 10:10:35 +02:00
Pierre Goiffon
d90b1a3d82 🐛 N°4020 Fix syntax error for PHP < 7.1
`syntax error, unexpected 'const' (T_CONST), expecting variable (T_VARIABLE) in /var/www/itop274/setup/compiler.class.inc.php on line 61`
Was added in 1059befa
2021-07-02 09:26:03 +02:00
acognet
76a237aad4 N°3930 - Polishing: Dashlets 2021-07-01 17:28:35 +02:00
Pierre Goiffon
3694108f42 N°3870 updateLicenses : fix generating wrong product names on Windows
Was including paths fragments.

Example :
<product scope="datamodels">C:\Dev\wamp64\www\itop-dev\.make\license/../..//datamodels/2.x/authent-cas/vendor/apereo/phpcas</product>

Instead of :
<product scope="datamodels">apereo/phpcas</product>
2021-07-01 17:20:55 +02:00
Eric
1a7755365c 🎨 Allow tab stickiness for twig based extensions 2021-07-01 16:08:03 +02:00
Pierre Goiffon
8cf75f826f 🔨 updateLicenses : add logs and replace rm -f by unlink() 2021-07-01 15:30:33 +02:00
Molkobain
a1da086a64 Revert "Fix and simplify dynamic dictionaries load in ajax pages"
This reverts commit e46743af
2021-07-01 13:38:16 +02:00
Molkobain
5d1d6d07a6 Fix regression introduced in 9048d09bf 2021-07-01 10:55:50 +02:00
Eric
e6a38a8055 N°3573 - Allow retrieving operation 2021-07-01 10:40:04 +02:00
Molkobain
3b3f1806ce Fix resources URL not being printed correctly
If there was any parameters in the URL it would not work as the `&` would have been printed as `&amp;`, see similar issue in the previous commit
2021-07-01 09:55:01 +02:00
Molkobain
e46743af2a Fix and simplify dynamic dictionaries load in ajax pages 2021-07-01 09:55:00 +02:00
Molkobain
9048d09bf6 Fix JS dictionaries not being load in \WebPage 2021-07-01 09:55:00 +02:00
Molkobain
3cd03729b9 Setup: Fix "Reload" button style when config file is read-only 2021-07-01 09:55:00 +02:00
Eric
ff760dacbe N°3573 - Allow disabling buttons 2021-06-30 17:05:03 +02:00
Thomas Casteleyn
94f662c71a Show real next backup date (#196)
* Get the next backup date from what is scheduled in the background task.
* Show different message if cron is not running.
2021-06-30 14:59:36 +02:00
Eric
c7d87ad5b0 🎨 Display human readable sizes in bytes as int instead of float 2021-06-30 11:18:08 +02:00
Pierre Goiffon
ad9726b64c 🔧 .editorConfig : restore old ij_visual_guides value
Was overwritten by mistake in 19505649
2021-06-30 10:24:19 +02:00
Pierre Goiffon
e32e275f40 🎨 Align dataprovider elements 2021-06-29 16:45:34 +02:00
Pierre Goiffon
195056492e 🔧 .editorConfig : enable var alignement 2021-06-29 16:45:18 +02:00
Eric
5691ca0327 Fix CI 2021-05-28 08:48:47 +02:00
653 changed files with 18359 additions and 14089 deletions

View File

@@ -280,7 +280,7 @@ ij_javascript_while_brace_force = always
ij_javascript_while_on_new_line = false
ij_javascript_wrap_comments = false
[{*.ctp,*.hphp,*.inc,*.module,*.php,*.php4,*.php5,*.phtml}]
[{*.ctp, *.hphp, *.inc, *.module, *.php, *.php4, *.php5, *.phtml}]
indent_style = tab
ij_continuation_indent_size = 4
ij_smart_tabs = true
@@ -289,8 +289,8 @@ ij_php_align_assignments = false
ij_php_align_class_constants = false
ij_php_align_group_field_declarations = false
ij_php_align_inline_comments = false
ij_php_align_key_value_pairs = false
ij_php_align_multiline_array_initializer_expression = false
ij_php_align_key_value_pairs = true
ij_php_align_multiline_array_initializer_expression = true
ij_php_align_multiline_binary_operation = false
ij_php_align_multiline_chained_methods = false
ij_php_align_multiline_extends_list = false

View File

@@ -10,10 +10,6 @@
* `curl -L -o /usr/bin/jq.exe https://github.com/stedolan/jq/releases/latest/download/jq-win64.exe`
* this is a Windows port : https://stedolan.github.io/jq/
*
* Known bug on Windows :
* Licenses added from Composer contains a path in the product node (N°3870)
* `<product scope="lib">C:\Dev\wamp64\www\itop-dev\.make\license/../..//lib/symfony/console</product>`
*
* Licenses sources :
* * `composer licenses --format json` (see https://getcomposer.org/doc/03-cli.md#licenses)
* * keep every existing nodes with `/licenses/license[11]/product/@scope` not in ['lib', 'datamodels']
@@ -70,39 +66,83 @@ function get_license_nodes($file_path)
$xp = new DOMXPath($dom);
$licenseList = $xp->query('/licenses/license');
$licenses = iterator_to_array($licenseList);
$licenses = iterator_to_array($licenseList);
usort($licenses, 'sort_by_product');
return $licenses;
}
/** @noinspection SuspiciousAssignmentsInspection */
function fix_product_name(DOMNode &$oProductNode)
{
$sProductNameOrig = $oProductNode->nodeValue;
// sample : `C:\Dev\wamp64\www\itop-27\.make\license/../..//lib/symfony/polyfill-ctype`
$sProductNameFixed = remove_dir_from_string($sProductNameOrig, 'lib/');
// sample : `C:\Dev\wamp64\www\itop-27\.make\license/../..//datamodels/2.x/authent-cas/vendor/apereo/phpcas`
$sProductNameFixed = remove_dir_from_string($sProductNameFixed, 'vendor/');
$oProductNode->nodeValue = $sProductNameFixed;
}
function remove_dir_from_string($sString, $sNeedle)
{
if (strpos($sString, $sNeedle) === false) {
return $sString;
}
$sStringTmp = strstr($sString, $sNeedle);
$sStringFixed = str_replace($sNeedle, '', $sStringTmp);
// DEBUG trace O:)
// echo "$sNeedle = $sString => $sStringFixed\n";
return $sStringFixed;
}
$old_licenses = get_license_nodes($xmlFilePath);
//generate file with updated licenses
$generated_license_file_path = __DIR__."/provfile.xml";
exec("bash " . __DIR__ . "/gen-community-license.sh $iTopFolder > ". $generated_license_file_path);
echo "- Generating licences...";
exec("bash ".__DIR__."/gen-community-license.sh $iTopFolder > ".$generated_license_file_path);
echo "OK!\n";
echo "- Get licenses nodes...";
$new_licenses = get_license_nodes($generated_license_file_path);
exec("rm -f ". $generated_license_file_path);
unlink($generated_license_file_path);
foreach ($old_licenses as $b) {
$aProductNode = get_product_node($b);
if (get_scope($aProductNode) !== "lib" && get_scope($aProductNode) !== "datamodels" )
{
if (get_scope($aProductNode) !== "lib" && get_scope($aProductNode) !== "datamodels") {
$new_licenses[] = $b;
}
}
usort($new_licenses, 'sort_by_product');
echo "OK!\n";
echo "- Overwritting Combodo license file...";
$new_dom = new DOMDocument("1.0");
$new_dom->formatOutput = true;
$root = $new_dom->createElement("licenses");
$new_dom->appendChild($root);
foreach ($new_licenses as $b) {
$node = $new_dom->importNode($b,true);
$root->appendChild($new_dom->importNode($b,true));
$node = $new_dom->importNode($b, true);
// N°3870 fix when running script in Windows
// fix should be in gen-community-license.sh but it is easier to do it here !
if (strncasecmp(PHP_OS, 'WIN', 3) === 0) {
$oProductNodeOrig = get_product_node($node);
fix_product_name($oProductNodeOrig);
}
$root->appendChild($node);
}
$new_dom->save($xmlFilePath);
$new_dom->save($xmlFilePath);
echo "OK!\n";

View File

@@ -202,8 +202,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
$sParams .= $sName.'='.urlencode($value).'&'; // Always add a trailing &
}
$sUrl = utils::GetAbsoluteUrlAppRoot().'pages/'.$oObj->GetUIPage().'?'.$sParams.'class='.get_class($oObj).'&id='.$oObj->getKey().'&'.$oAppContext->GetForLink().'&a=1';
$oPage->add_script(
<<<EOF
$oPage->add_early_script(<<<JS
if (!sessionStorage.getItem('$sSessionStorageKey'))
{
sessionStorage.setItem('$sSessionStorageKey', 1);
@@ -213,7 +212,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
{
sessionStorage.removeItem('$sSessionStorageKey');
}
EOF
JS
);
$oObj->Reload();
@@ -662,7 +661,7 @@ HTML
}
$oClassIcon = new MedallionIcon(MetaModel::GetClassIcon($sTargetClass, false));
$oClassIcon->SetDescription($oAttDef->GetDescription())->AddCSSClass('ibo-blocklist--medallion');
$oClassIcon->SetDescription($oAttDef->GetDescription())->AddCSSClass('ibo-block-list--medallion');
$oPage->AddUiBlock($oClassIcon);
$sDisplayValue = ''; // not used
@@ -740,7 +739,7 @@ HTML
);
}
$oClassIcon = new MedallionIcon(MetaModel::GetClassIcon($sTargetClass, false));
$oClassIcon->SetDescription($oAttDef->GetDescription())->AddCSSClass('ibo-blocklist--medallion');
$oClassIcon->SetDescription($oAttDef->GetDescription())->AddCSSClass('ibo-block-list--medallion');
$oPage->AddUiBlock($oClassIcon);
$oBlock = new DisplayBlock($oLinkSet->GetFilter(), 'list', false);
$oBlock->Display($oPage, 'rel_'.$sAttCode, $aParams);
@@ -797,7 +796,7 @@ HTML
foreach($aNotificationClasses as $sNotifClass) {
$oClassIcon = new MedallionIcon(MetaModel::GetClassIcon($sNotifClass, false));
$oClassIcon->SetDescription(MetaModel::GetName($sNotifClass))->AddCSSClass('ibo-blocklist--medallion');
$oClassIcon->SetDescription(MetaModel::GetName($sNotifClass))->AddCSSClass('ibo-block-list--medallion');
$oPage->AddUiBlock($oClassIcon);
$oBlock = new DisplayBlock($aNotifSearches[$sNotifClass], 'list', false);
@@ -1036,9 +1035,32 @@ HTML
*/
public function DisplayDetails(WebPage $oPage, $bEditMode = false, $sMode = self::ENUM_OBJECT_MODE_VIEW)
{
// N°3786: As this can now be call recursively from the self::ReloadAndDisplay(), we need to make sure we don't fall into an infinite loop
static $bBlockReentrance = false;
$sClass = get_class($this);
$iKey = $this->GetKey();
if ($sMode === static::ENUM_OBJECT_MODE_VIEW)
{
// The concurrent access lock makes sense only for already existing objects
$LockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled');
if ($LockEnabled)
{
$aLockInfo = iTopOwnershipLock::IsLocked($sClass, $iKey);
if ($aLockInfo['locked'] === true && $aLockInfo['owner']->GetKey() == UserRights::GetUserId() && $bBlockReentrance === false)
{
// If the object is locked by the current user, it's worth trying again, since
// the lock may be released by 'onunload' which is called AFTER loading the current page.
//$bTryAgain = $oOwner->GetKey() == UserRights::GetUserId();
$bBlockReentrance = true;
self::ReloadAndDisplay($oPage, $this, array('operation' => 'details'));
return;
}
}
}
// Object's details
$oObjectDetails = ObjectFactory::MakeDetails($this);
@@ -1123,25 +1145,11 @@ HTML
*/
public static function GetDisplaySetForPrinting(WebPage $oPage, DBObjectSet $oSet, $aExtraParams = array())
{
$sTableId = isset($aExtraParams['table_id']) ? $aExtraParams['table_id'] : null;
$bViewLink = true;
$sSelectMode = 'none';
$iListId = $sTableId;
$sClassAlias = $oSet->GetClassAlias();
$sClassName = $oSet->GetClass();
$sZListName = 'list';
$aClassAliases = array($sClassAlias => $sClassName);
$aList = cmdbAbstractObject::FlattenZList(MetaModel::GetZListItems($sClassName, $sZListName));
$oDataTable = new PrintableDataTable($iListId, $oSet, $aClassAliases, $sTableId);
$oSettings = DataTableSettings::GetDataModelSettings($aClassAliases, $bViewLink, array($sClassAlias => $aList));
$oSettings->iDefaultPageSize = 0;
$oSettings->aSortOrder = MetaModel::GetOrderByDefault($sClassName);
return $oDataTable->Display($oPage, $oSettings, false /* $bDisplayMenu */, $sSelectMode, $bViewLink,
$aExtraParams);
$sTableId = isset($aExtraParams['table_id']) ? $aExtraParams['table_id'] : utils::GetUniqueId();;
$aExtraParams['view_link'] = true;
$aExtraParams['select_mode'] = 'none';
return DataTableUIBlockFactory::MakeForObject($oPage, $sTableId, $oSet, $aExtraParams);
}
/**
@@ -2403,6 +2411,7 @@ HTML;
case 'CustomFields':
$sHTMLValue .= '<div id="'.$iId.'_console_form">';
$sHTMLValue .= '<div id="'.$iId.'_field_set">';
$sHTMLValue .= '</div></div>';
$sHTMLValue .= '<div>'.$sReloadSpan.'</div>'; // No validation span for this one: it does handle its own validation!
$sHTMLValue .= "<input name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" type=\"hidden\" id=\"$iId\" value=\"\"/>\n";
@@ -2533,14 +2542,13 @@ EOF
$sDisplayValueForHtml = utils::EscapeHtml($sDisplayValue);
// Adding tooltip so we can read the whole value when its very long (eg. URL)
$sTip = '';
$sTip = '';
if (!empty($sDisplayValue)) {
$sTip = 'data-tooltip-content="'.$sDisplayValueForHtml.'"';
$oPage->add_ready_script(
<<<EOF
$oPage->add_ready_script(<<<JS
$('#{$iId}').on('keyup', function(evt, sFormId){
var sVal = $('#{$iId}').val();
var oTippy = this._tippy;
let sVal = $('#{$iId}').val();
const oTippy = this._tippy;
if(sVal === '')
{
@@ -2553,7 +2561,7 @@ EOF
}
oTippy.setContent(sVal);
});
EOF
JS
);
}
@@ -3096,55 +3104,45 @@ EOF
// The list of candidate fields is made of the ordered list of "details" attributes + other attributes
$aAttributes = array();
foreach($this->FlattenZList(MetaModel::GetZListItems($sClass, 'details')) as $sAttCode)
{
foreach ($this->FlattenZList(MetaModel::GetZListItems($sClass, 'details')) as $sAttCode) {
$aAttributes[$sAttCode] = true;
}
foreach(MetaModel::GetAttributesList($sClass) as $sAttCode)
{
if (!array_key_exists($sAttCode, $aAttributes))
{
foreach (MetaModel::GetAttributesList($sClass) as $sAttCode) {
if (!array_key_exists($sAttCode, $aAttributes)) {
$aAttributes[$sAttCode] = true;
}
}
// Order the fields based on their dependencies, set the fields for which there is only one possible value
// and perform this in the order of dependencies to avoid dead-ends
$aDeps = array();
foreach($aAttributes as $sAttCode => $trash)
{
foreach ($aAttributes as $sAttCode => $trash) {
$aDeps[$sAttCode] = MetaModel::GetPrerequisiteAttributes($sClass, $sAttCode);
}
$aList = $this->OrderDependentFields($aDeps);
foreach($aList as $sAttCode)
{
$bExistFieldToDisplay = false;
foreach ($aList as $sAttCode) {
// Consider only the "expected" fields for the target state
if (array_key_exists($sAttCode, $aExpectedAttributes))
{
if (array_key_exists($sAttCode, $aExpectedAttributes)) {
$iExpectCode = $aExpectedAttributes[$sAttCode];
// Prompt for an attribute if
// - the attribute must be changed or must be displayed to the user for confirmation
// - or the field is mandatory and currently empty
if (($iExpectCode & (OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) ||
(($iExpectCode & OPT_ATT_MANDATORY) && ($this->Get($sAttCode) == '')))
{
(($iExpectCode & OPT_ATT_MANDATORY) && ($this->Get($sAttCode) == ''))) {
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
$aArgs = array('this' => $this);
// If the field is mandatory, set it to the only possible value
if ((!$oAttDef->IsNullAllowed()) || ($iExpectCode & OPT_ATT_MANDATORY))
{
if ($oAttDef->IsExternalKey())
{
if ((!$oAttDef->IsNullAllowed()) || ($iExpectCode & OPT_ATT_MANDATORY)) {
if ($oAttDef->IsExternalKey()) {
/** @var DBObjectSet $oAllowedValues */
$oAllowedValues = MetaModel::GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs, '',
$this->Get($sAttCode));
if ($oAllowedValues->CountWithLimit(2) == 1)
{
if ($oAllowedValues->CountWithLimit(2) == 1) {
$oRemoteObj = $oAllowedValues->Fetch();
$this->Set($sAttCode, $oRemoteObj->GetKey());
}
}
else
} else
{
$aAllowedValues = MetaModel::GetAllowedValues_att($sClass, $sAttCode, $aArgs);
if (is_array($aAllowedValues) && count($aAllowedValues) == 1)
@@ -3180,8 +3178,7 @@ EOF
$bExcludeRawValue = false;
foreach (static::GetAttDefClassesToExcludeFromMarkupMetadataRawValue() as $sAttDefClassToExclude)
{
if (is_a($sAttDefClass, $sAttDefClassToExclude, true))
{
if (is_a($sAttDefClass, $sAttDefClassToExclude, true)) {
$bExcludeRawValue = true;
break;
}
@@ -3191,88 +3188,105 @@ EOF
$aDetails[] = $aAttrib;
$aFieldsMap[$sAttCode] = 'att_'.$iFieldIndex;
$iFieldIndex++;
$bExistFieldToDisplay = true;
}
}
}
$oPage->set_title($sActionLabel);
$oPage->add(<<<HTML
<!-- Beginning of object-transition -->
<div class="object-transition" data-object-class="$sClass" data-object-id="$iKey" data-object-mode="$sMode" data-object-current-state="$sCurrentState" data-object-target-state="$sTargetState">
if ($bExistFieldToDisplay) {
$oPage->set_title($sActionLabel);
$oPage->add(<<<HTML
<!-- Beginning of object-transition -->
<div class="object-transition" data-object-class="$sClass" data-object-id="$iKey" data-object-mode="$sMode" data-object-current-state="$sCurrentState" data-object-target-state="$sTargetState">
HTML
);
);
// Page title and subtitles
$oPage->AddUiBlock(TitleUIBlockFactory::MakeForPage($sActionLabel.' - '.$this->GetName()));
if (!empty($sActionDetails)) {
$oPage->AddUiBlock(TitleUIBlockFactory::MakeForPage($sActionDetails));
}
// Page title and subtitles
$oPage->AddUiBlock(TitleUIBlockFactory::MakeForPage($sActionLabel.' - '.$this->GetRawName()));
if (!empty($sActionDetails)) {
$oPage->AddUiBlock(TitleUIBlockFactory::MakeForPage($sActionDetails));
}
$oFormContainer = new UIContentBlock(null, ['ibo-wizard-container']);
$oPage->AddUiBlock($oFormContainer);
$oForm = new Combodo\iTop\Application\UI\Base\Component\Form\Form('apply_stimulus');
$oFormContainer->AddSubBlock($oForm);
$oFormContainer = new UIContentBlock(null, ['ibo-wizard-container']);
$oPage->AddUiBlock($oFormContainer);
$oForm = new Combodo\iTop\Application\UI\Base\Component\Form\Form('apply_stimulus');
$oFormContainer->AddSubBlock($oForm);
$oForm->SetOnSubmitJsCode("return OnSubmit('apply_stimulus');")
->AddSubBlock(InputUIBlockFactory::MakeForHidden('id', $this->GetKey(), 'id'))
->AddSubBlock(InputUIBlockFactory::MakeForHidden('class', $sClass))
->AddSubBlock(InputUIBlockFactory::MakeForHidden('operation', 'apply_stimulus'))
->AddSubBlock(InputUIBlockFactory::MakeForHidden('stimulus', $sStimulus))
->AddSubBlock(InputUIBlockFactory::MakeForHidden('transaction_id', $iTransactionId));
$oForm->SetOnSubmitJsCode("return OnSubmit('apply_stimulus');")
->AddSubBlock(InputUIBlockFactory::MakeForHidden('id', $this->GetKey(), 'id'))
->AddSubBlock(InputUIBlockFactory::MakeForHidden('class', $sClass))
->AddSubBlock(InputUIBlockFactory::MakeForHidden('operation', 'apply_stimulus'))
->AddSubBlock(InputUIBlockFactory::MakeForHidden('stimulus', $sStimulus))
->AddSubBlock(InputUIBlockFactory::MakeForHidden('transaction_id', $iTransactionId));
if ($sOwnershipToken !== null) {
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('ownership_token', utils::HtmlEntities($sOwnershipToken)));
}
if ($sOwnershipToken !== null) {
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('ownership_token', utils::HtmlEntities($sOwnershipToken)));
}
// Note: Remove the table is we want fields to occupy the whole width of the container
$oForm->AddHtml('<table><tr><td>');
$oForm->AddHtml($oPage->GetDetails($aDetails));
$oForm->AddHtml('</td></tr></table>');
// Note: Remove the table is we want fields to occupy the whole width of the container
$oForm->AddHtml('<table><tr><td>');
$oForm->AddHtml($oPage->GetDetails($aDetails));
$oForm->AddHtml('</td></tr></table>');
$oAppContext = new ApplicationContext();
$oForm->AddHtml($oAppContext->GetForForm());
$oAppContext = new ApplicationContext();
$oForm->AddHtml($oAppContext->GetForForm());
$oCancelButton = ButtonUIBlockFactory::MakeForCancel(Dict::S('UI:Button:Cancel'), 'cancel', 'cancel');
$oCancelButton->SetOnClickJsCode("BackToDetails('{$sClass}', '{$this->GetKey()}', '', '{$sOwnershipToken}');");
$oForm->AddSubBlock($oCancelButton);
$oCancelButton = ButtonUIBlockFactory::MakeForCancel(Dict::S('UI:Button:Cancel'), 'cancel', 'cancel');
$oCancelButton->SetOnClickJsCode("BackToDetails('{$sClass}', '{$this->GetKey()}', '', '{$sOwnershipToken}');");
$oForm->AddSubBlock($oCancelButton);
$oSubmitButton = ButtonUIBlockFactory::MakeForPrimaryAction($sActionLabel, 'submit', 'submit', true);
$oForm->AddSubBlock($oSubmitButton);
$oSubmitButton = ButtonUIBlockFactory::MakeForPrimaryAction($sActionLabel, 'submit', 'submit', true);
$oForm->AddSubBlock($oSubmitButton);
$oPage->add(<<<HTML
<!-- End of object-transition -->
</div>
$oPage->add(<<<HTML
<!-- End of object-transition -->
</div>
HTML
);
);
$iFieldsCount = count($aFieldsMap);
$sJsonFieldsMap = json_encode($aFieldsMap);
$iFieldsCount = count($aFieldsMap);
$sJsonFieldsMap = json_encode($aFieldsMap);
$oPage->add_script(
<<<EOF
// Initializes the object once at the beginning of the page...
var oWizardHelper = new WizardHelper('$sClass', '', '$sTargetState', '{$this->GetState()}', '$sStimulus');
oWizardHelper.SetFieldsMap($sJsonFieldsMap);
oWizardHelper.SetFieldsCount($iFieldsCount);
$oPage->add_script(
<<<EOF
// Initializes the object once at the beginning of the page...
var oWizardHelper = new WizardHelper('$sClass', '', '$sTargetState', '{$this->GetState()}', '$sStimulus');
oWizardHelper.SetFieldsMap($sJsonFieldsMap);
oWizardHelper.SetFieldsCount($iFieldsCount);
EOF
);
$sJSToken = json_encode($sOwnershipToken);
$oPage->add_ready_script(
<<<EOF
// Starts the validation when the page is ready
CheckFields('apply_stimulus', false);
$(window).on('unload', function() { return OnUnload('$iTransactionId', '$sClass', $iKey, $sJSToken) } );
);
$sJSToken = json_encode($sOwnershipToken);
$oPage->add_ready_script(
<<<EOF
// Starts the validation when the page is ready
CheckFields('apply_stimulus', false);
$(window).on('unload', function() { return OnUnload('$iTransactionId', '$sClass', $iKey, $sJSToken) } );
EOF
);
);
if ($sOwnershipToken !== null)
{
$this->GetOwnershipJSHandler($oPage, $sOwnershipToken);
if ($sOwnershipToken !== null) {
$this->GetOwnershipJSHandler($oPage, $sOwnershipToken);
}
// Note: This part (inline images activation) is duplicated in self::DisplayModifyForm and several other places. Maybe it should be refactored so it automatically activates when an HTML field is present, or be an option of the attribute. See bug N°1240.
$sTempId = utils::GetUploadTempId($iTransactionId);
$oPage->add_ready_script(InlineImage::EnableCKEditorImageUpload($this, $sTempId));
} else {
//we can directly apply the stimuli
$bApplyStimulus = $this->ApplyStimulus($sStimulus); // will write the object in the DB
if (!$bApplyStimulus) {
throw new ApplicationException(Dict::S('UI:FailedToApplyStimuli'));
} else {
if ($sOwnershipToken !== null) {
// Release the concurrent lock, if any
iTopOwnershipLock::ReleaseLock($sClass, $iKey, $sOwnershipToken);
}
return true;
}
}
// Note: This part (inline images activation) is duplicated in self::DisplayModifyForm and several other places. Maybe it should be refactored so it automatically activates when an HTML field is present, or be an option of the attribute. See bug N°1240.
$sTempId = utils::GetUploadTempId($iTransactionId);
$oPage->add_ready_script(InlineImage::EnableCKEditorImageUpload($this, $sTempId));
return false;
}
public static function ProcessZlist($aList, $aDetails, $sCurrentTab, $sCurrentCol, $sCurrentSet)
@@ -3587,7 +3601,7 @@ HTML;
* @api
* @overwritable-hook
*
* @param $sFinalClass string The actual class of the objects for which to display the menu
* @param string $sFinalClass The actual class of the objects for which to display the menu
*
* @return array the list of menu codes (i.e dictionary entries) that can be displayed as shortcuts next to the
* actions menu
@@ -5343,6 +5357,20 @@ EOF
$sJSOk = json_encode(Dict::S('UI:Button:Ok'));
$oPage->add_ready_script(
<<<JS
// Prepare reusable modal
const oOwnershipLockModal = $('<div></div>').dialog({
title: $sJSTitle,
modal: true,
autoOpen: false,
minWidth: 600,
buttons:[{
text: $sJSOk,
class: 'ibo-is-alternative',
click: function() { $(this).dialog('close'); }
}],
close: function() { $(this).dialog('close'); }
});
// Start periodic handler
let hOwnershipLockHandlerInterval = window.setInterval(function() {
if (window.bInSubmit || window.bInCancel) return;
@@ -5352,18 +5380,8 @@ EOF
if ($('.lock_owned').length == 0)
{
$('.ui-layout-content').prepend('<div class="header_message message_error lock_owned">'+data.message+'</div>');
$('<div>'+data.popup_message+'</div>').dialog({
title: $sJSTitle,
modal: true,
autoOpen: true,
minWidth: 600,
buttons:[{
text: {$sJSOk},
class: 'ibo-is-alternative',
click: function() { $(this).dialog('close'); }
}],
close: function() { $(this).remove(); }
});
oOwnershipLockModal.text(data.popup_message);
oOwnershipLockModal.dialog('open');
}
$('.object-details form .ibo-toolbar .ibo-button:not([name="cancel"])').prop('disabled', true);
clearInterval(hOwnershipLockHandlerInterval);
@@ -5373,18 +5391,8 @@ EOF
if ($('.lock_owned').length == 0)
{
$('.ui-layout-content').prepend('<div class="header_message message_error lock_owned">'+data.message+'</div>');
$('<div>'+data.popup_message+'</div>').dialog({
title: $sJSTitle,
modal: true,
autoOpen: true,
minWidth: 600,
buttons:[{
text: $sJSOk,
class: 'ibo-is-alternative',
click: function() { $(this).dialog('close'); }
}],
close: function() { $(this).remove(); }
});
oOwnershipLockModal.text(data.popup_message);
oOwnershipLockModal.dialog('open');
}
$('.object-details form .ibo-toolbar .ibo-button:not([name="cancel"])').prop('disabled', true);
clearInterval(hOwnershipLockHandlerInterval);

View File

@@ -906,18 +906,22 @@ class DashletObjectList extends Dashlet
$sShowMenu = $this->aProperties['menu'] ? '1' : '0';
$oFilter = $this->GetDBSearch($aExtraParams);
$sClass = $oFilter->GetClass();
$oPanel = PanelUIBlockFactory::MakeForClass($sClass, Dict::S($sTitle))
->AddCSSClass('ibo-datatable-panel');
//$oPanel = PanelUIBlockFactory::MakeForClass($sClass, Dict::S($sTitle))
// ->AddCSSClass('ibo-datatable-panel');
$oBlock = new DisplayBlock($oFilter, 'list');
$aParams = array(
'menu' => $sShowMenu,
'table_id' => self::APPUSERPREFERENCES_PREFIX.$this->sId,
'surround_with_panel' => false,
'surround_with_panel' => true,
'max_height' => '500px',
"panel_title" => Dict::S($sTitle),
"panel_class" => $sClass,
);
$sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occurring in the same DOM)
$oBlock->DisplayIntoContentBlock($oPanel, $oPage, $sBlockId, array_merge($aExtraParams, $aParams));
//$oBlock->DisplayIntoContentBlock($oPanel, $oPage, $sBlockId, array_merge($aExtraParams, $aParams));
$oPanel = $oBlock->GetDisplay($oPage, $sBlockId, array_merge($aExtraParams, $aParams));
return $oPanel;
}
@@ -984,6 +988,7 @@ HTML;
$oField = new DesignerLongTextField('query', Dict::S('UI:DashletObjectList:Prop-Query'), $this->aProperties['query']);
$oField->SetMandatory();
$oField->AddCSSClass("ibo-queryoql");
$oForm->AddField($oField);
$oField = new DesignerBooleanField('menu', Dict::S('UI:DashletObjectList:Prop-Menu'), $this->aProperties['menu']);
@@ -1020,6 +1025,7 @@ HTML;
$oField = new DesignerHiddenField('query', Dict::S('UI:DashletObjectList:Prop-Query'), $sOQL);
$oField->SetMandatory();
$oField->AddCSSClass("ibo-queryoql");
$oForm->AddField($oField);
$oField = new DesignerBooleanField('menu', Dict::S('UI:DashletObjectList:Prop-Menu'), $this->aProperties['menu']);
@@ -1256,15 +1262,21 @@ abstract class DashletGroupBy extends Dashlet
break;
}
$oPanel = PanelUIBlockFactory::MakeForClass($sClass, Dict::S($sTitle));
//$oPanel = \Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory::MakeStandard();
//PanelUIBlockFactory::MakeForClass($sClass, Dict::S($sTitle));
$sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occurring in the same DOM)
$oBlock = new DisplayBlock($oFilter, $sType);
$oBlock->DisplayIntoContentBlock($oPanel, $oPage, $sBlockId, array_merge($aExtraParams, $aParams));
//$oBlock->DisplayIntoContentBlock($oPanel, $oPage, $sBlockId, array_merge($aExtraParams, $aParams));
$aExtraParams["surround_with_panel"] = true;
$aExtraParams["panel_title"] = Dict::S($sTitle);
$aExtraParams["panel_class"] = $sClass;
$oPanel = $oBlock->GetDisplay($oPage, $sBlockId, array_merge($aExtraParams, $aParams));
if ($bEditMode) {
$oPanel->AddHtml('<div class="dashlet-blocker"></div>');
$oPanel->AddHtml('<div class="ibo-dashlet-blocker dashlet-blocker"></div>');
}
return $oPanel;
}
@@ -1363,10 +1375,10 @@ abstract class DashletGroupBy extends Dashlet
$oField = new DesignerLongTextField('query', Dict::S('UI:DashletGroupBy:Prop-Query'), $this->aProperties['query']);
$oField->SetMandatory();
$oField->AddCSSClass("ibo-queryoql");
$oForm->AddField($oField);
try
{
try {
// Group by field: build the list of possible values (attribute codes + ...)
$aGroupBy = $this->GetGroupByOptions($this->aProperties['query']);
@@ -1620,16 +1632,14 @@ abstract class DashletGroupBy extends Dashlet
$oField = new DesignerHiddenField('query', Dict::S('UI:DashletGroupBy:Prop-Query'), $sOQL);
$oField->SetMandatory();
$oField->AddCSSClass("ibo-queryoql");
$oForm->AddField($oField);
if (!is_null($sOQL))
{
if (!is_null($sOQL)) {
$oField = new DesignerComboField('group_by', Dict::S('UI:DashletGroupBy:Prop-GroupBy'), null);
$aGroupBy = $this->GetGroupByOptions($sOQL);
$oField->SetAllowedValues($aGroupBy);
}
else
{
} else {
// Creating a form for reading parameters!
$oField = new DesignerTextField('group_by', Dict::S('UI:DashletGroupBy:Prop-GroupBy'), null);
}
@@ -2173,6 +2183,7 @@ class DashletHeaderDynamic extends Dashlet
$oField = new DesignerLongTextField('query', Dict::S('UI:DashletHeaderDynamic:Prop-Query'), $this->aProperties['query']);
$oField->SetMandatory();
$oField->AddCSSClass("ibo-queryoql");
$oForm->AddField($oField);
try

View File

@@ -10,6 +10,7 @@ use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletFactory;
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Pill\PillFactory;
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu;
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuItem\PopoverMenuItemFactory;
@@ -171,6 +172,8 @@ class DisplayBlock
/**positive or negative*/
'max_height',
/** string Max. height of the list, if not specified will occupy all the available height no matter the pagination */
'localize_values',
/** param for export.php */
], DataTableUIBlockFactory::GetAllowedParams()),
'list_search' => array_merge([
'update_history',
@@ -189,6 +192,8 @@ class DisplayBlock
/** string */
'open',
/** bool open by default the search */
'submit_on_load',
/** bool submit the search on loading page */
'class', /** class name */
'search_header_force_dropdown', /** Html for <select> to choose the class to search */
'this',
@@ -198,6 +203,8 @@ class DisplayBlock
/** string search root class */
'open',
/** bool open the search panel by default */
'submit_on_load',
/** bool submit the search on loading page */
'result_list_outer_selector',
/** string js selector of the search result display */
'search_header_force_dropdown',
@@ -257,6 +264,12 @@ class DisplayBlock
'withJSRefreshCallBack',
/** true if dashboard page */
'from_dashboard_page',
/** bool true if list may be render in panel block */
'surround_with_panel',
/** string title of panel block */
'panel_title',
/** string class for panel block style */
'panel_class',
];
if (isset($aAllowedParams[$sStyle])) {
@@ -1026,10 +1039,12 @@ JS
$sHyperlink = $aCount['link'];
$sCountLabel = $aCount['label'];
$oPill = PillFactory::MakeForState($sClass, $sStateValue)
->SetUrl($sHyperlink)
->SetTooltip($sStateLabel)
->AddHtml("<span class=\"ibo-dashlet-header-dynamic--count\">$sCountLabel</span>")
->AddHtml("<span class=\"ibo-dashlet-header-dynamic--label ibo-text-truncated-with-ellipsis\">$sStateLabel</span>");
if ($sHyperlink != '-') {
$oPill->SetUrl($sHyperlink);
}
$oBlock->AddSubBlock($oPill);
}
$aExtraParams['query_params'] = $this->m_oFilter->GetInternalParams();
@@ -1173,11 +1188,20 @@ JS
),
);
$sFormat = isset($aExtraParams['format']) ? $aExtraParams['format'] : 'UI:Pagination:HeaderNoSelection';
$sTitle = Dict::Format($sFormat, $iTotalCount);
$aExtraParams['query_params'] = $this->m_oFilter->GetInternalParams();
$aOption['dom'] = 'pl';
$oBlock = DataTableUIBlockFactory::MakeForStaticData($sTitle, $aAttribs, $aData, null, $aExtraParams, $this->m_oFilter->ToOQL(), $aOption);
if (isset($aExtraParams["surround_with_panel"]) && $aExtraParams["surround_with_panel"]) {
$sTitle = Dict::Format($sFormat, $iTotalCount);
$oBlock = PanelUIBlockFactory::MakeForClass($aExtraParams["panel_class"], $aExtraParams["panel_title"]);
$oBlock->AddSubTitleBlock(new Html($sTitle));
$oDataTable = DataTableUIBlockFactory::MakeForStaticData("", $aAttribs, $aData, null, $aExtraParams, $this->m_oFilter->ToOQL(), $aOption);
$oBlock->AddSubBlock($oDataTable);
} else {
$sTitle = Dict::Format($sFormat, $iTotalCount);
$oBlock = DataTableUIBlockFactory::MakeForStaticData($sTitle, $aAttribs, $aData, null, $aExtraParams, $this->m_oFilter->ToOQL(), $aOption);
}
} else {
// Simply count the number of elements in the set
@@ -1186,7 +1210,12 @@ JS
if (isset($aExtraParams['format'])) {
$sFormat = $aExtraParams['format'];
}
$oBlock = new Html('<p>'.Dict::Format($sFormat, $iCount).'</p>');
if (isset($aExtraParams["surround_with_panel"]) && $aExtraParams["surround_with_panel"]) {
$oBlock = PanelUIBlockFactory::MakeForClass($aExtraParams["panel_class"], $aExtraParams["panel_title"]);
$oBlock->AddSubBlock(new Html('<p>'.Dict::Format($sFormat, $iCount).'</p>'));
} else {
$oBlock = new Html('<p>'.Dict::Format($sFormat, $iCount).'</p>');
}
}
return $oBlock;
@@ -1273,6 +1302,22 @@ JS
$oBlock->bNotAuthorized = true;
}
} else {
if (isset($aExtraParams['update_history']) && true == $aExtraParams['update_history']) {
$sSearchFilter = $this->m_oSet->GetFilter()->serialize();
// Limit the size of the URL (N°1585 - request uri too long)
if (strlen($sSearchFilter) < SERVER_MAX_URL_LENGTH) {
$oBlock->sEventAttachedData = json_encode(array(
'filter' => $sSearchFilter,
'breadcrumb_id' => "ui-search-".$this->m_oSet->GetClass(),
'breadcrumb_label' => MetaModel::GetName($this->m_oSet->GetClass()),
'breadcrumb_max_count' => utils::GetConfig()->Get('breadcrumb.max_count'),
'breadcrumb_instance_id' => MetaModel::GetConfig()->GetItopInstanceid(),
'breadcrumb_icon' => 'fas fa-search',
'breadcrumb_icon_type' => iTopWebPage::ENUM_BREADCRUMB_ENTRY_ICON_TYPE_CSS_CLASSES,
));
}
}
// The list is made of only 1 class of objects, actions on the list are possible
if (($this->m_oSet->CountWithLimit(1) > 0) && (UserRights::IsActionAllowed($this->m_oSet->GetClass(), UR_ACTION_READ, $this->m_oSet) == UR_ALLOWED_YES)) {
$oBlock->AddSubBlock(cmdbAbstractObject::GetDisplaySetBlock($oPage, $this->m_oSet, $aExtraParams));
@@ -1299,25 +1344,16 @@ JS
$oBlock->bCreateNew = true;
}
}
}
if (isset($aExtraParams['update_history']) && true == $aExtraParams['update_history']) {
$sSearchFilter = $this->m_oSet->GetFilter()->serialize();
// Limit the size of the URL (N°1585 - request uri too long)
if (strlen($sSearchFilter) < SERVER_MAX_URL_LENGTH) {
$oBlock->sEventAttachedData = json_encode(array(
'filter' => $sSearchFilter,
'breadcrumb_id' => "ui-search-".$this->m_oSet->GetClass(),
'breadcrumb_label' => MetaModel::GetName($this->m_oSet->GetClass()),
'breadcrumb_max_count' => utils::GetConfig()->Get('breadcrumb.max_count'),
'breadcrumb_instance_id' => MetaModel::GetConfig()->GetItopInstanceid(),
'breadcrumb_icon' => 'fas fa-search',
'breadcrumb_icon_type' => iTopWebPage::ENUM_BREADCRUMB_ENTRY_ICON_TYPE_CSS_CLASSES,
));
if (isset($aExtraParams["surround_with_panel"]) && $aExtraParams["surround_with_panel"]) {
$oPanel = PanelUIBlockFactory::MakeForClass($aExtraParams["panel_class"], $aExtraParams["panel_title"]);
$oPanel->AddSubBlock($oBlock);
return $oPanel;
}
}
}
}
return $oBlock;
}
@@ -1506,6 +1542,13 @@ JS
$oBlock->sUrl = $sUrl;
if (isset($aExtraParams["surround_with_panel"]) && $aExtraParams["surround_with_panel"]) {
$oPanel = PanelUIBlockFactory::MakeForClass($aExtraParams["panel_class"], $aExtraParams["panel_title"]);
$oPanel->AddSubBlock($oBlock);
return $oPanel;
}
return $oBlock;
}
@@ -1587,6 +1630,13 @@ JS
$oBlock->sURLForRefresh = str_replace("'", "\'", $sUrl);
break;
}
if (isset($aExtraParams["surround_with_panel"]) && $aExtraParams["surround_with_panel"]) {
$oPanel = PanelUIBlockFactory::MakeForClass($aExtraParams["panel_class"], $aExtraParams["panel_title"]);
$oPanel->AddSubBlock($oBlock);
return $oPanel;
}
return $oBlock;
}
@@ -2158,9 +2208,9 @@ class MenuBlock extends DisplayBlock
// Extract favorite actions from their menus
$aFavoriteRegularActions = [];
$aFavoriteTransitionActions = [];
$aCallSpec = [$sClass, 'GetShortcutActions'];
if (is_callable($aCallSpec)) {
$aShortcutActions = call_user_func($aCallSpec, $sClass);
if (is_callable([$sClass, 'GetShortcutActions'])) {
/** @var cmdbAbstractObject $sClass */
$aShortcutActions = $sClass::GetShortcutActions($sClass);
foreach ($aShortcutActions as $key) {
// Regular actions
if (isset($aRegularActions[$key])) {

View File

@@ -105,7 +105,7 @@ class DesignerForm
foreach($aFields as $oField) {
$aRow = $oField->Render($oP, $sFormId);
if ($oField->IsVisible()) {
$sValidation = '<span class="prop_apply ibo-prop--apply">'.$this->GetValidationArea($oField->GetFieldId()).'</span>';
$sValidation = '<span class="prop_apply ibo-prop--apply ibo-button ibo-is-alternative">'.$this->GetValidationArea($oField->GetFieldId()).'</span>';
$sField = $aRow['value'].$sValidation;
$aDetails[] = array('label' => $aRow['label'], 'value' => $sField);
} else {
@@ -203,51 +203,41 @@ class DesignerForm
$sActionUrl = addslashes($this->sSubmitTo);
$sJSSubmitParams = json_encode($this->aSubmitParams);
$sFormId = $this->GetFormId();
if ($this->oParentForm == null)
{
if ($this->oParentForm == null) {
$sReturn = '<form id="'.$sFormId.'" onsubmit="return false;">';
$sReturn .= '<table class="prop_table">';
$sReturn .= '<thead><tr><th class="prop_header">'.Dict::S('UI:Form:Property').'</th><th class="prop_header">'.Dict::S('UI:Form:Value').'</th><th colspan="2" class="prop_header">&nbsp;</th></tr></thead><tbody>';
$sReturn .= '<thead><tr><th class="ibo-prop-header">'.Dict::S('UI:Form:Property').'</th><th class="ibo-prop-header">'.Dict::S('UI:Form:Value').'</th><th colspan="2" class="ibo-prop-header">&nbsp;</th></tr></thead><tbody>';
}
$sHiddenFields = '';
foreach($this->aFieldSets as $sLabel => $aFields)
{
foreach ($this->aFieldSets as $sLabel => $aFields) {
$aDetails = array();
if ($sLabel != '')
{
if ($sLabel != '') {
$sReturn .= $this->StartRow().'<th colspan="4">'.$sLabel.'</th>'.$this->EndRow();
}
foreach($aFields as $oField)
{
foreach ($aFields as $oField) {
$aRow = $oField->Render($oP, $sFormId, 'property');
if ($oField->IsVisible())
{
if ($oField->IsVisible()) {
$sFieldId = $this->GetFieldId($oField->GetCode());
$sValidation = $this->GetValidationArea($sFieldId, '<span data-tooltip-content="Apply"><i class="fas fa-check"></i></span>');
$sValidationFields = '</td><td class="prop_icon prop_apply ibo-prop--apply">'.$sValidation.'</td><td class="prop_icon prop_cancel ibo-prop--cancel"><span data-tooltip-content="Revert"><i class="fas fa-times"></i></span></td>'.$this->EndRow();
$sPath = $this->GetHierarchyPath().'/'.$oField->GetCode();
if (is_null($aRow['label']))
{
$sValidation = $this->GetValidationArea($sFieldId, '<span data-tooltip-content="'.Dict::Format('UI:DashboardEdit:Apply').'"><i class="fas fa-check"></i></span>');
$sValidationFields = '</td><td class="prop_icon prop_apply ibo-prop--apply ibo-button ibo-is-alternative" >'.$sValidation.'</td><td class="prop_icon prop_cancel ibo-prop--cancel ibo-button ibo-is-alternative"><span data-tooltip-content="'.Dict::Format('UI:DashboardEdit:Revert').'"><i class="fas fa-times"></i></span></td>'
.$this->EndRow();
if (is_null($aRow['label'])) {
$sReturn .= $this->StartRow($sFieldId).'<td class="prop_value ibo-field--value" colspan="2">'.$aRow['value'];
}
else
{
} else {
$sReturn .= $this->StartRow($sFieldId).'<td class="prop_label ibo-field--label">'.$aRow['label'].'</td><td class="prop_value ibo-field--value">'.$aRow['value'];
}
if (!($oField instanceof DesignerFormSelectorField) && !($oField instanceof DesignerMultipleSubFormField))
{
if (!($oField instanceof DesignerFormSelectorField) && !($oField instanceof DesignerMultipleSubFormField)) {
$sReturn .= $sValidationFields;
}
$sNotifyParentSelectorJS = is_null($sNotifyParentSelector) ? 'null' : "'".addslashes($sNotifyParentSelector)."'";
$sAutoApply = $oField->IsAutoApply() ? 'true' : 'false';
$sHandlerEquals = $oField->GetHandlerEquals();
$sHandlerGetValue = $oField->GetHandlerGetValue();
$sWidgetClass = $oField->GetWidgetClass();
$sJSExtraParams = '';
if (count($oField->GetWidgetExtraParams()) > 0)
@@ -1423,28 +1413,32 @@ class DesignerIconSelectionField extends DesignerFormField
$sId = $this->oForm->GetFieldId($this->sCode);
$sName = $this->oForm->GetFieldName($this->sCode);
$idx = 0;
foreach($this->aAllowedValues as $index => $aValue)
{
if ($aValue['value'] == $this->defaultValue)
{
$idxFallback = 0;
foreach ($this->aAllowedValues as $index => $aValue) {
if ($aValue['value'] == $this->defaultValue) {
$idx = $index;
break;
}
//fallback if url of default value contains ../
//for contact, icon is http://localhost/env-production/itop-structure/../../images/icons/icons8-customer.svg => not found http://localhost/images/icons/icons8-customer.svg
if (basename($aValue['value']) == basename($this->defaultValue)) {
$idxFallback = $index;
}
}
if ($idx == 0) {
$idx = $idxFallback;
}
$sJSItems = json_encode($this->aAllowedValues);
$sPostUploadTo = ($this->sUploadUrl == null) ? 'null' : "'{$this->sUploadUrl}'";
if (!$this->IsReadOnly())
{
if (!$this->IsReadOnly()) {
$sDefaultValue = ($this->defaultValue !== '') ? $this->defaultValue : $this->aAllowedValues[$idx]['value'];
$sValue = "<input type=\"hidden\" id=\"$sId\" name=\"$sName\" value=\"{$sDefaultValue}\"/>";
$oP->add_ready_script(
<<<EOF
<<<EOF
$('#$sId').icon_select({current_idx: $idx, items: $sJSItems, post_upload_to: $sPostUploadTo});
EOF
);
}
else
{
} else {
$sValue = '<span style="display:inline-block;line-height:48px;height:48px;"><span><img style="vertical-align:middle" src="'.$this->aAllowedValues[$idx]['icon'].'" />&nbsp;'.htmlentities($this->aAllowedValues[$idx]['label'], ENT_QUOTES, 'UTF-8').'</span></span>';
}
$sReadOnly = $this->IsReadOnly() ? 'disabled' : '';
@@ -1459,18 +1453,21 @@ class RunTimeIconSelectionField extends DesignerIconSelectionField
public function __construct($sCode, $sLabel = '', $defaultValue = '')
{
parent::__construct($sCode, $sLabel, $defaultValue);
$aFolderList = [
APPROOT.'env-'.utils::GetCurrentEnvironment() => utils::GetAbsoluteUrlModulesRoot(),
APPROOT.'images/icons' => utils::GetAbsoluteUrlAppRoot().'images/icons',
];
if (count(self::$aAllIcons) == 0) {
foreach ($aFolderList as $sFolderPath => $sUrlPrefix) {
$aIcons = self::FindIconsOnDisk($sFolderPath);
ksort($aIcons);
if (count(self::$aAllIcons) == 0)
{
self::$aAllIcons = self::FindIconsOnDisk(APPROOT.'env-'.utils::GetCurrentEnvironment());
ksort(self::$aAllIcons);
foreach ($aIcons as $sFilePath) {
self::$aAllIcons[] = array('value' => $sFilePath, 'label' => basename($sFilePath), 'icon' => $sUrlPrefix.$sFilePath);
}
}
}
$aValues = array();
foreach(self::$aAllIcons as $sFilePath)
{
$aValues[] = array('value' => $sFilePath, 'label' => basename($sFilePath), 'icon' => utils::GetAbsoluteUrlModulesRoot().$sFilePath);
}
$this->SetAllowedValues($aValues);
$this->SetAllowedValues(self::$aAllIcons);
}
static protected function FindIconsOnDisk($sBaseDir, $sDir = '')
@@ -1501,26 +1498,29 @@ class RunTimeIconSelectionField extends DesignerIconSelectionField
SetupUtils::builddir(dirname($sCacheFile));
file_put_contents($sCacheFile, $sAvailableIcons, LOCK_EX);
}
return $aFiles;
}
static protected function _FindIconsOnDisk($sBaseDir, $sDir = '')
static protected function _FindIconsOnDisk($sBaseDir, $sDir = '', &$aFilesSpecs = [])
{
$aResult = array();
$aResult = [];
// Populate automatically the list of icon files
if ($hDir = @opendir($sBaseDir.'/'.$sDir))
{
while (($sFile = readdir($hDir)) !== false)
{
if ($hDir = @opendir($sBaseDir.'/'.$sDir)) {
while (($sFile = readdir($hDir)) !== false) {
$aMatches = array();
if (($sFile != '.') && ($sFile != '..') && ($sFile != 'lifecycle') && is_dir($sBaseDir.'/'.$sDir.'/'.$sFile))
{
if (($sFile != '.') && ($sFile != '..') && ($sFile != 'lifecycle') && is_dir($sBaseDir.'/'.$sDir.'/'.$sFile)) {
$sDirSubPath = ($sDir == '') ? $sFile : $sDir.'/'.$sFile;
$aResult = array_merge($aResult, self::_FindIconsOnDisk($sBaseDir, $sDirSubPath));
$aResult = array_merge($aResult, self::_FindIconsOnDisk($sBaseDir, $sDirSubPath, $aFilesSpecs));
}
if (preg_match("/\.(png|jpg|jpeg|gif)$/i", $sFile, $aMatches)) // png, jp(e)g and gif are considered valid
$sSize = filesize($sBaseDir.'/'.$sDir.'/'.$sFile);
if (isset($aFilesSpecs[$sFile]) && $aFilesSpecs[$sFile] == $sSize) {
continue;
}
if (preg_match("/\.(png|jpg|jpeg|gif|svg)$/i", $sFile, $aMatches)) // png, jp(e)g, gif and svg are considered valid
{
$aResult[$sFile.'_'.$sDir] = $sDir.'/'.$sFile;
$aFilesSpecs[$sFile] = $sSize;
}
}
closedir($hDir);
@@ -1645,27 +1645,23 @@ class DesignerFormSelectorField extends DesignerFormField
{
$sId = $this->oForm->GetFieldId($this->sCode);
$sName = $this->oForm->GetFieldName($this->sCode);
$sReadOnly = $this->IsReadOnly() ? 'disabled="disabled"' : '';
$sReadOnly = $this->IsReadOnly() ? 'disabled="disabled"' : '';
$this->aCSSClasses[] = 'formSelector';
$sCSSClasses = '';
if (count($this->aCSSClasses) > 0)
{
if (count($this->aCSSClasses) > 0) {
$sCSSClasses = 'class="'.implode(' ', $this->aCSSClasses).'"';
}
if ($this->IsSorted())
{
if ($this->IsSorted()) {
uasort($this->aSubForms, array(get_class($this), 'SortOnFormLabel'));
}
if ($this->IsReadOnly())
{
if ($this->IsReadOnly()) {
$sDisplayValue = '';
$sHiddenValue = '';
foreach($this->aSubForms as $iKey => $aFormData)
{
foreach ($this->aSubForms as $iKey => $aFormData) {
if ($iKey == $this->defaultValue) // Default value is actually the index
{
$sDisplayValue = htmlentities($aFormData['label'], ENT_QUOTES, 'UTF-8');
@@ -1674,12 +1670,9 @@ class DesignerFormSelectorField extends DesignerFormField
}
}
$sHtml = "<span $sCSSClasses>".$sDisplayValue.$sHiddenValue."</span>";
}
else
{
} else {
$sHtml = "<select $sCSSClasses id=\"$sId\" name=\"$sName\" $sReadOnly>";
foreach($this->aSubForms as $iKey => $aFormData)
{
foreach ($this->aSubForms as $iKey => $aFormData) {
$sDisplayValue = htmlentities($aFormData['label'], ENT_QUOTES, 'UTF-8');
$sValue = htmlentities($aFormData['value'], ENT_QUOTES, 'UTF-8');
$sSelected = ($iKey == $this->defaultValue) ? 'selected' : '';
@@ -1687,22 +1680,19 @@ class DesignerFormSelectorField extends DesignerFormField
}
$sHtml .= "</select>";
}
if ($sRenderMode == 'property')
{
$sHtml .= '</td><td class="prop_icon prop_apply"><span title="Apply" class="ui-icon ui-icon-circle-check"/></td><td class="prop_icon prop_cancel"><span title="Revert" class="ui-icon ui-icon-circle-close"/></td></tr>';
if ($sRenderMode == 'property') {
$sHtml .= '</td><td class="prop_icon prop_apply ibo-prop--apply ibo-button ibo-is-alternative"><span data-tooltip-content="'.Dict::Format('UI:DashboardEdit:Apply').'"><i class="fas fa-check"></i></span></td><td class="prop_icon prop_cancel ibo-prop--cancel ibo-button ibo-is-alternative"><span data-tooltip-content="'.Dict::Format('UI:DashboardEdit:Revert').'"><i class="fas fa-times"></i></span></td></tr>';
}
foreach($this->aSubForms as $sKey => $aFormData)
{
foreach ($this->aSubForms as $sKey => $aFormData) {
$sId = $this->oForm->GetFieldId($this->sCode);
$sStyle = (($sKey == $this->defaultValue) && $this->oForm->IsDisplayed()) ? '' : 'style="display:none"';
$oSubForm = $aFormData['form'];
$oSubForm->SetParentForm($this->oForm);
$oSubForm->CopySubmitParams($this->oForm);
$oSubForm->SetPrefix($this->oForm->GetPrefix().$sKey.'_');
if ($sRenderMode == 'property')
{
if ($sRenderMode == 'property') {
// Note: Managing the visibility of nested subforms had several implications
// 1) Attributes are displayed in a table and we have to group them in as many tbodys as necessary to hide/show the various options depending on the current selection
// 2) It is not possible to nest tbody tags. Therefore, it is not possible to manage the visibility the same way as it is done for the dialog mode (using nested divs).

View File

@@ -1094,9 +1094,13 @@ class LoginWebPage extends NiceWebPage
if (isset($_SESSION['auth_user']))
{
$sAuthUser = $_SESSION['auth_user'];
$sIssue = $_SESSION['pwd_issue'] ?? null;
unset($_SESSION['pwd_issue']);
$bFailedLogin = ($sIssue != null); // Force the "failed login" flag to display the "issue" message
UserRights::Login($sAuthUser); // Set the user's language
$oPage = self::NewLoginWebPage();
$oPage->DisplayChangePwdForm();
$oPage->DisplayChangePwdForm($bFailedLogin, $sIssue);
$oPage->output();
exit;
}

View File

@@ -1128,22 +1128,11 @@ class OQLMenuNode extends MenuNode
//$sIcon = MetaModel::GetClassIcon($oSearch->GetClass(), false);
if ($bSearchPane) {
$aParams = array_merge(array('open' => $bSearchOpen, 'table_id' => $sUsageId), $aExtraParams);
$aParams = array_merge(['open' => $bSearchOpen, 'table_id' => $sUsageId, 'submit_on_load' => true], $aExtraParams);
$oBlock = new DisplayBlock($oSearch, 'search', false /* Asynchronous */, $aParams);
$oBlock->Display($oPage, 0);
}
//$oPage->add("<p class=\"page-header\">$sIcon ".utils::HtmlEntities(Dict::S($sTitle))."</p>");
$oPage->add("<div class='sf_results_area' data-target='search_results'>");
$oTitle = TitleUIBlockFactory::MakeForPage($sTitle);
$oPage->AddUiBlock($oTitle);
$aParams = array_merge(array('table_id' => $sUsageId), $aExtraParams);
$oBlock = new DisplayBlock($oSearch, 'list', false /* Asynchronous */, $aParams);
$oBlock->Display($oPage, $sUsageId);
$oPage->add("</div>");
if ($bEnableBreadcrumb && ($oPage instanceof iTopWebPage)) {
// Breadcrumb
//$iCount = $oBlock->GetDisplayedCount();
@@ -1209,7 +1198,8 @@ class SearchMenuNode extends MenuNode
$oPage->SetBreadCrumbEntry("menu-".$this->sMenuId, $this->GetTitle(), '', '', 'fas fa-search', iTopWebPage::ENUM_BREADCRUMB_ENTRY_ICON_TYPE_CSS_CLASSES);
$oSearch = new DBObjectSearch($this->sClass);
$aParams = array_merge(array('table_id' => 'Menu_'.utils::GetSafeId($this->GetMenuId())), $aExtraParams);
$sUsageId = 'Menu_'.utils::GetSafeId($this->GetMenuId());
$aParams = array_merge(array('table_id' =>$sUsageId), $aExtraParams);
$oBlock = new DisplayBlock($oSearch, 'search', false /* Asynchronous */, $aParams);
$oBlock->Display($oPage, 0);
}

View File

@@ -27,6 +27,7 @@ class ThemeHandler
{
const IMAGE_EXTENSIONS = ['png', 'gif', 'jpg', 'jpeg'];
/** @var \CompileCSSService */
private static $oCompileCSSService;
public static function GetAppRootWithSlashes()
@@ -315,11 +316,6 @@ class ThemeHandler
// Loading files to import and stylesheet to compile, also getting most recent modification time on overall files
$sTmpThemeScssContent = '';
$oFindStylesheetObject = new FindStylesheetObject();
if (isset($aThemeParameters['variable_imports'])) {
foreach ($aThemeParameters['variable_imports'] as $sImport) {
static::FindStylesheetFile($sImport, $aImportsPaths, $oFindStylesheetObject);
}
}
if (isset($aThemeParameters['utility_imports'])) {
foreach ($aThemeParameters['utility_imports'] as $sImport) {
@@ -337,6 +333,12 @@ class ThemeHandler
$sTmpThemeScssContent .= '@import "'.$sStylesheet.'";'."\n";
}
if (isset($aThemeParameters['variable_imports'])) {
foreach ($aThemeParameters['variable_imports'] as $sImport) {
static::FindStylesheetFile($sImport, $aImportsPaths, $oFindStylesheetObject);
}
}
$iStyleLastModified = $oFindStylesheetObject->GetLastModified();
$aIncludedImages=static::GetIncludedImages($aThemeParametersWithVersion, $oFindStylesheetObject->GetAllStylesheetPaths(), $sThemeId);

View File

@@ -82,6 +82,12 @@ class UIExtKeyWidget
$aArgs = [], $bSearchMode = false, &$sInputType = ''
)
{
// we will only use key & name, so let's reduce fields loaded !
$aAttToLoad = [
$sClass => [], // nothing, id and friendlyname are automatically added by the API
];
$oAllowedValues->OptimizeColumnLoad($aAttToLoad);
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
$sTargetClass = $oAttDef->GetTargetClass();
$iMaxComboLength = $oAttDef->GetMaximumComboLength();
@@ -246,7 +252,7 @@ class UIExtKeyWidget
}
$sInputType = CmdbAbstractObject::ENUM_INPUT_TYPE_DROPDOWN_DECORATED;
$sHTMLValue .= "<select title=\"$sHelpText\" name=\"{$sAttrFieldPrefix}{$sFieldName}\" id=\"$this->iId\" tabindex=\"0\"></select>";
$sJsonOptions = json_encode($aOptions);
$sJsonOptions = str_replace('\\', '\\\\', json_encode($aOptions));
$oPage->add_ready_script(
<<<EOF
oACWidget_{$this->iId} = new ExtKeyWidget('{$this->iId}', '{$this->sTargetClass}', '$sFilter', '$sTitle', true, $sWizHelper, '{$this->sAttCode}', $sJSSearchMode, $sJSDoSearch);
@@ -976,7 +982,7 @@ JS
public function DisplayHierarchy(WebPage $oPage, $sFilter, $currValue, $oObj)
{
$sDialogTitle = addslashes(Dict::Format('UI:HierarchyOf_Class', MetaModel::GetName($this->sTargetClass)));
$oPage->add('<div id="dlg_tree_'.$this->iId.'"><div class="wizContainer" style="vertical-align:top;"><div style="overflow:auto;background:#fff;margin-bottom:5px;" id="tree_'.$this->iId.'">');
$oPage->add('<div id="dlg_tree_'.$this->iId.'"><div class="wizContainer" style="vertical-align:top;"><div style="margin-bottom:5px;" id="tree_'.$this->iId.'">');
$oPage->add('<table style="width:100%"><tr><td>');
if (is_null($sFilter))
{

View File

@@ -538,7 +538,7 @@ class utils
*/
public static function ReadMultipleSelection($oFullSetFilter)
{
$aSelectedObj = utils::ReadParam('selectObject[]', array());
$aSelectedObj = utils::ReadParam('selectObject', array());
$sSelectionMode = utils::ReadParam('selectionMode', '');
if ($sSelectionMode != '') {
// Paginated selection
@@ -2638,9 +2638,21 @@ class utils
if(!empty($aMentionsAllowedClasses)) {
$aDefaultConf['mentions'] = [];
foreach($aMentionsAllowedClasses as $sMentionChar => $sMentionClass) {
foreach($aMentionsAllowedClasses as $sMentionMarker => $sMentionScope) {
// Retrieve mention class
// - First test if the conf is a simple Datamodel class
if (MetaModel::IsValidClass($sMentionScope)) {
$sMentionClass = $sMentionScope;
}
// - Otherwise it must be a valid OQL
else {
$oTmpSearch = DBSearch::FromOQL($sMentionScope);
$sMentionClass = $oTmpSearch->GetClass();
unset($oTmpSearch);
}
// Note: Endpoints are defaults only and should be overloaded by other GUIs such as the end-users portal
$sMentionEndpoint = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=cke_mentions&target_class='.$sMentionClass.'&needle={encodedQuery}';
$sMentionEndpoint = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=cke_mentions&marker='.$sMentionMarker.'&needle={encodedQuery}';
$sMentionItemUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&class='.$sMentionClass.'&id={id}';
$sMentionItemPictureTemplate = (empty(MetaModel::GetImageAttributeCode($sMentionClass))) ? '' : <<<HTML
@@ -2650,12 +2662,12 @@ HTML;
<li class="ibo-vendors-ckeditor--autocomplete-item" data-id="{id}">{$sMentionItemPictureTemplate}<span class="ibo-vendors-ckeditor--autocomplete-item-title">{friendlyname}</span></li>
HTML;
$sMentionOutputTemplate = <<<HTML
<a href="$sMentionItemUrl" data-role="object-mention" data-object-class="{class}" data-object-id="{id}">{$sMentionChar}{friendlyname}</a>
<a href="$sMentionItemUrl" data-role="object-mention" data-object-class="{class}" data-object-id="{id}">{$sMentionMarker}{friendlyname}</a>
HTML;
$aDefaultConf['mentions'][] = [
'feed' => $sMentionEndpoint,
'marker' => $sMentionChar,
'marker' => $sMentionMarker,
'minChars' => MetaModel::GetConfig()->Get('min_autocomplete_chars'),
'itemTemplate' => $sMentionItemTemplate,
'outputTemplate' => $sMentionOutputTemplate,
@@ -2814,15 +2826,30 @@ HTML;
* Check if iTop is in a development environment (VCS vs build number)
*
* @return bool
*
* @since 2.6.0 method creation
* @since 3.0.0 add the `developer_mode.enabled` config parameter
*
* @use `developer_mode.enabled` config parameter
* @use ITOP_REVISION
*/
public static function IsDevelopmentEnvironment()
{
if (! defined('ITOP_REVISION')) {
$oConfig = utils::GetConfig();
$bIsDevEnvInConfig = $oConfig->Get('developer_mode.enabled');
if ($bIsDevEnvInConfig === true) {
return true;
}
if ($bIsDevEnvInConfig === false) {
return false;
}
if (!defined('ITOP_REVISION')) {
//defensive behaviour: by default we are not in dev environment
//can happen even in production (unattended install for example) or with exotic use of iTop
return false;
}
return ITOP_REVISION === 'svn';
}

View File

@@ -11,7 +11,7 @@
"ext-mysqli": "*",
"ext-soap": "*",
"combodo/tcpdf": "6.3.5",
"nikic/php-parser": "^3.1",
"nikic/php-parser": "^4.12.0",
"pear/archive_tar": "1.4.13",
"pelago/emogrifier": "2.1.0",
"scssphp/scssphp": "1.0.6",
@@ -50,11 +50,7 @@
"classmap": [
"core",
"application",
"sources/application",
"sources/Composer",
"sources/Controller",
"sources/Form",
"sources/Renderer"
"sources"
],
"exclude-from-classmap": [
"core/dbobjectsearch.class.php",

23
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "62e394b1ef30b4e716e3e3e519de11dd",
"content-hash": "75f17b71005971207815906ec7e9cf67",
"packages": [
{
"name": "combodo/tcpdf",
@@ -68,24 +68,25 @@
},
{
"name": "nikic/php-parser",
"version": "v3.1.5",
"version": "v4.12.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce"
"reference": "6608f01670c3cc5079e18c1dab1104e002579143"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/bb87e28e7d7b8d9a7fda231d37457c9210faf6ce",
"reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6608f01670c3cc5079e18c1dab1104e002579143",
"reference": "6608f01670c3cc5079e18c1dab1104e002579143",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
"php": ">=5.5"
"php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0|~5.0"
"ircmaxell/php-yacc": "^0.0.7",
"phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
},
"bin": [
"bin/php-parse"
@@ -93,7 +94,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "4.9-dev"
}
},
"autoload": {
@@ -115,7 +116,11 @@
"parser",
"php"
],
"time": "2018-02-28T20:30:58+00:00"
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v4.12.0"
},
"time": "2021-07-21T10:44:31+00:00"
},
{
"name": "paragonie/random_compat",

View File

@@ -388,7 +388,12 @@ class AsyncSendEmail extends AsyncTask
return "Bug - the email should be sent in synchronous mode";
case EMAIL_SEND_ERROR:
return "Failed: ".implode(', ', $aIssues);
if (is_array($aIssues)) {
$sMessage = "Sending eMail failed: ".implode(', ', $aIssues);
} else {
$sMessage = "Sending eMail failed.";
}
throw new Exception($sMessage);
}
return '';
}

View File

@@ -4159,13 +4159,13 @@ class AttributeText extends AttributeString
$sValue = parent::GetAsHTML($sValue, $oHostObject, $bLocalize);
$sValue = self::RenderWikiHtml($sValue);
return "<div $sStyle>".str_replace("\n", "<br>\n", $sValue).'</div>';
return "<div $sStyle>$sValue</div>";
}
else
{
$sValue = self::RenderWikiHtml($sValue, true /* wiki only */);
return "<div class=\"HTML\" $sStyle>".InlineImage::FixUrls($sValue).'</div>';
return "<div class=\"HTML ibo-is-html-content\" $sStyle>".InlineImage::FixUrls($sValue).'</div>';
}
}

View File

@@ -1,22 +1,4 @@
<?php
// Copyright (C) 2010-2021 Combodo SARL
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* Persistent class (internal) cmdbChange
*
@@ -24,6 +6,7 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Core\CMDBChange\CMDBChangeOrigin;
/**
* A change as requested/validated at once by user, may groups many atomic changes
@@ -53,7 +36,7 @@ class CMDBChange extends DBObject
MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeExternalKey("user_id", array("allowed_values"=>null, "sql"=>"user_id", "targetclass"=>"User", "is_null_allowed"=>true, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeEnum("origin", array("allowed_values"=>new ValueSetEnum('interactive,csv-interactive,csv-import.php,webservice-soap,webservice-rest,synchro-data-source,email-processing,custom-extension'), "sql"=>"origin", "default_value"=>"interactive", "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeEnum("origin", array("allowed_values"=>new ValueSetEnum(implode(',', [CMDBChangeOrigin::INTERACTIVE, CMDBChangeOrigin::CSV_INTERACTIVE, CMDBChangeOrigin::CSV_IMPORT, CMDBChangeOrigin::WEBSERVICE_SOAP, CMDBChangeOrigin::WEBSERVICE_REST, CMDBChangeOrigin::SYNCHRO_DATA_SOURCE, CMDBChangeOrigin::EMAIL_PROCESSING, CMDBChangeOrigin::CUSTOM_EXTENSION])), "sql"=>"origin", "default_value"=>CMDBChangeOrigin::INTERACTIVE, "is_null_allowed"=>true, "depends_on"=>array())));
}
/**

View File

@@ -606,7 +606,7 @@ class CMDBSource
{
$sShortSQL = substr(preg_replace("/\s+/", " ", substr($sSql, 0, 180)), 0, 150);
if (substr_compare($sShortSQL, "SELECT", 0, strlen("SELECT")) !== 0) {
IssueLog::Trace("$sShortSQL", 'cmdbsource');
IssueLog::Trace("$sShortSQL", LogChannels::CMDB_SOURCE);
}
$oKPI = new ExecutionKPI();
@@ -695,12 +695,11 @@ class CMDBSource
$aStackTrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT , 3);
$sCaller = 'From '.$aStackTrace[1]['file'].'('.$aStackTrace[1]['line'].'): '.$aStackTrace[2]['class'].'->'.$aStackTrace[2]['function'].'()';
$bHasExistingTransactions = self::IsInsideTransaction();
if (!$bHasExistingTransactions)
{
IssueLog::Trace("START TRANSACTION $sCaller", 'cmdbsource');
if (!$bHasExistingTransactions) {
IssueLog::Trace("START TRANSACTION $sCaller", LogChannels::CMDB_SOURCE);
self::DBQuery('START TRANSACTION');
} else {
IssueLog::Trace("Ignore nested (".self::$m_iTransactionLevel.") START TRANSACTION $sCaller", 'cmdbsource');
IssueLog::Trace("Ignore nested (".self::$m_iTransactionLevel.") START TRANSACTION $sCaller", LogChannels::CMDB_SOURCE);
}
self::AddTransactionLevel();
@@ -720,21 +719,20 @@ class CMDBSource
{
$aStackTrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT , 3);
$sCaller = 'From '.$aStackTrace[1]['file'].'('.$aStackTrace[1]['line'].'): '.$aStackTrace[2]['class'].'->'.$aStackTrace[2]['function'].'()';
if (!self::IsInsideTransaction())
{
if (!self::IsInsideTransaction()) {
// should not happen !
IssueLog::Error("No Transaction COMMIT $sCaller", 'cmdbsource');
IssueLog::Error("No Transaction COMMIT $sCaller", LogChannels::CMDB_SOURCE);
throw new MySQLNoTransactionException('Trying to commit transaction whereas none have been started !', null);
}
self::RemoveLastTransactionLevel();
if (self::IsInsideTransaction())
{
IssueLog::Trace("Ignore nested (".self::$m_iTransactionLevel.") COMMIT $sCaller", 'cmdbsource');
if (self::IsInsideTransaction()) {
IssueLog::Trace("Ignore nested (".self::$m_iTransactionLevel.") COMMIT $sCaller", LogChannels::CMDB_SOURCE);
return;
}
IssueLog::Trace("COMMIT $sCaller", 'cmdbsource');
IssueLog::Trace("COMMIT $sCaller", LogChannels::CMDB_SOURCE);
self::DBQuery('COMMIT');
}
@@ -755,20 +753,19 @@ class CMDBSource
{
$aStackTrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT , 3);
$sCaller = 'From '.$aStackTrace[1]['file'].'('.$aStackTrace[1]['line'].'): '.$aStackTrace[2]['class'].'->'.$aStackTrace[2]['function'].'()';
if (!self::IsInsideTransaction())
{
if (!self::IsInsideTransaction()) {
// should not happen !
IssueLog::Error("No Transaction ROLLBACK $sCaller", 'cmdbsource');
IssueLog::Error("No Transaction ROLLBACK $sCaller", LogChannels::CMDB_SOURCE);
throw new MySQLNoTransactionException('Trying to commit transaction whereas none have been started !', null);
}
self::RemoveLastTransactionLevel();
if (self::IsInsideTransaction())
{
IssueLog::Trace("Ignore nested (".self::$m_iTransactionLevel.") ROLLBACK $sCaller", 'cmdbsource');
if (self::IsInsideTransaction()) {
IssueLog::Trace("Ignore nested (".self::$m_iTransactionLevel.") ROLLBACK $sCaller", LogChannels::CMDB_SOURCE);
return;
}
IssueLog::Trace("ROLLBACK $sCaller", 'cmdbsource');
IssueLog::Trace("ROLLBACK $sCaller", LogChannels::CMDB_SOURCE);
self::DBQuery('ROLLBACK');
}
@@ -904,11 +901,12 @@ class CMDBSource
/**
* @param string $sSql
* @param int $iMode
*
* @return array
* @throws \MySQLException if query cannot be processed
*/
public static function QueryToArray($sSql)
public static function QueryToArray($sSql, $iMode = MYSQLI_BOTH)
{
$aData = array();
$oKPI = new ExecutionKPI();
@@ -927,7 +925,7 @@ class CMDBSource
throw new MySQLException('Failed to issue SQL query', array('query' => $sSql));
}
while ($aRow = $oResult->fetch_array(MYSQLI_BOTH))
while ($aRow = $oResult->fetch_array($iMode))
{
$aData[] = $aRow;
}

View File

@@ -1237,6 +1237,30 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'activity_panel.prefilter_state_changes_on_logs' => [
'type' => 'bool',
'description' => 'Whether the "State changes" filter should be set by default on all log tabs.',
'default' => false,
'value' => false,
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'activity_panel.prefilter_edits_on_logs' => [
'type' => 'bool',
'description' => 'Whether the "Edits" filter should be set by default on all log tabs.',
'default' => false,
'value' => false,
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'activity_panel.hide_avatars' => [
'type' => 'array',
'description' => 'GUIs IDs ("backoffice", "itop-portal" for the standard end-users portal, ...) in which the user avatars should be hidden and replaced if possible by their initials (eg. array("backoffice", "itop-portal", "another-portal-id"))',
'default' => [],
'value' => [],
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'activity_panel.show_author_name_below_entries' => [
'type' => 'bool',
'description' => 'Whether or not to show the author friendlyname next to the date on the last entry.',
@@ -1263,9 +1287,9 @@ class Config
],
'mentions.allowed_classes' => [
'type' => 'array',
'description' => 'Classes which can be mentioned through the autocomplete in the caselogs. Key of the array must be a single character that will trigger the autocomplete (eg. "@" => "Person")',
'description' => 'Classes which can be mentioned through the autocomplete in the caselogs. Key of the array must be a single character that will trigger the autocomplete, value can be either a DM class or a valid OQL (eg. "@" => "Person", "?" => "SELECT FAQ WHERE status = \'published\'")',
'default' => [
'@' => 'Person',
'@' => 'SELECT Person WHERE status = \'active\'',
],
'value' => false,
'source_of_value' => '',
@@ -1399,6 +1423,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => true,
],
'developer_mode.enabled' => [
'type' => 'bool',
'description' => 'If true then unlocks dev env functionalities, see \utils::IsDevelopmentEnvironment',
'default' => null,
'value' => null,
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'theme.enable_precompilation' => [
'type' => 'bool',
'description' => 'If false, theme compilation will not use any precompiled file setup optimization.)',

View File

@@ -1277,13 +1277,20 @@ abstract class DBObject implements iDisplay
{
// If the object if not issued from a query but constructed programmatically
// the label may be empty. In this case run a query to get the object's friendly name
$oTmpObj = MetaModel::GetObject($sObjClass, $sObjKey, false);
if (is_object($oTmpObj))
{
$sObjOql = 'SELECT '.$sObjClass.' WHERE id='.$sObjKey;
$oObjFilter = DBSearch::FromOQL($sObjOql);
$oSet = new DBObjectSet($oObjFilter);
// we will only use id and friendlyname, so let's remove other fields !
$aAttToLoad = [
$sObjClass => [],
];
$oSet->OptimizeColumnLoad($aAttToLoad);
$oTmpObj = $oSet->Fetch();
if (is_object($oTmpObj)) {
$sHtmlLabel = $oTmpObj->GetName();
}
else
{
} else {
// May happen in case the target object is not in the list of allowed values for this attribute
$sHtmlLabel = "<em>$sObjClass::$sObjKey</em>";
}
@@ -1859,9 +1866,7 @@ abstract class DBObject implements iDisplay
{
/** @var \AttributeExternalKey $oAtt */
$sTargetClass = $oAtt->GetTargetClass();
$oTargetObj = MetaModel::GetObject($sTargetClass, $toCheck, false /*must be found*/, true /*allow all data*/);
if (is_null($oTargetObj))
{
if (false === MetaModel::IsObjectInDB($sTargetClass, $toCheck)) {
return "Target object not found ($sTargetClass::$toCheck)";
}
}

View File

@@ -63,22 +63,23 @@ else
abstract class DBSearch
{
/** @internal */
/** @internal */
const JOIN_POINTING_TO = 0;
/** @internal */
/** @internal */
const JOIN_REFERENCED_BY = 1;
protected $m_bNoContextParameters = false;
/** @var array For {@see iQueryModifier} impl */
protected $m_aModifierProperties = array();
protected $m_bArchiveMode = false;
protected $m_bShowObsoleteData = true;
/**
* DBSearch constructor.
*
* @api
* @see DBSearch::FromOQL()
*/
/**
* DBSearch constructor.
*
* @api
* @see DBSearch::FromOQL()
*/
public function __construct()
{
$this->Init();

View File

@@ -242,7 +242,7 @@ class DeletionPlan
public function SetDeletionIssues($oObject, $aIssues, $bSecurityIssue)
{
if (count($aIssues) > 0)
if (count($aIssues ?? []) > 0)
{
$sClass = get_class($oObject);
$iId = $oObject->GetKey();

View File

@@ -1448,7 +1448,7 @@ class DisplayableGraph extends SimpleGraph
$oP->add("<div class=\"not-printable\">\n");
$oUiSearchBlock = new Panel($sSftShort, [],Panel::ENUM_COLOR_CYAN, 'ds_flash');
$oUiSearchBlock->SetCSSClasses(["ibo-search-form-panel", "display_block"]);
$oUiSearchBlock->SetIsCollapsible(true);
$oUiHtmlBlock = new Combodo\iTop\Application\UI\Base\Component\Html\Html(
<<<EOF
<div id="ds_flash" class="search_box ibo-display-graph--search-box">

View File

@@ -6,6 +6,8 @@
*/
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\Variable;
use PhpParser\Parser;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter\Standard;
@@ -80,38 +82,49 @@ class iTopConfigParser
* @param \PhpParser\Parser $oParser
* @param $sConfig
*
* @return \Combodo\iTop\Config\Validator\ConfigNodesVisitor
* @return void
*/
private function BrowseFile(\PhpParser\Parser $oParser, $sConfig)
private function BrowseFile(Parser $oParser, $sConfig)
{
$prettyPrinter = new Standard();
try
{
try {
$aNodes = $oParser->parse($sConfig);
}
catch (\Error $e)
{
catch (\Error $e) {
$sMessage = Dict::Format('config-parse-error', $e->getMessage(), $e->getLine());
$this->oException = new \Exception($sMessage, 0, $e);
}
foreach ($aNodes as $oAssignation)
{
if (! $oAssignation instanceof Assign)
{
foreach ($aNodes as $sKey => $oNode) {
// With PhpParser 3 we had an Assign node at root
// In PhpParser 4 the root node is now an Expression
if (false === ($oNode instanceof \PhpParser\Node\Stmt\Expression)) {
continue;
}
/** @var \PhpParser\Node\Stmt\Expression $oNode */
if (false === ($oNode->expr instanceof Assign)) {
continue;
}
/** @var Assign $oAssignation */
$oAssignation = $oNode->expr;
if (false === ($oAssignation->var instanceof Variable)) {
continue;
}
if (false === ($oAssignation->expr instanceof PhpParser\Node\Expr\Array_)) {
continue;
}
$sCurrentRootVar = $oAssignation->var->name;
if (!array_key_exists($sCurrentRootVar, $this->aVarsMap))
{
if (!array_key_exists($sCurrentRootVar, $this->aVarsMap)) {
continue;
}
$aCurrentRootVarMap =& $this->aVarsMap[$sCurrentRootVar];
foreach ($oAssignation->expr->items as $oItem)
{
foreach ($oAssignation->expr->items as $oItem) {
$sValue = $prettyPrinter->prettyPrintExpr($oItem->value);
$aCurrentRootVarMap[$oItem->key->value] = $sValue;
}

View File

@@ -577,7 +577,7 @@ JS
oEditor.on( 'instanceReady', function() {
if(!CKEDITOR.env.iOS && $('#'+oEditor.id+'_toolbox .ibo-vendors-ckeditor--toolbar-fullscreen-button').length == 0)
{
$('#'+oEditor.id+'_toolbox').append('<span class="ibo-vendors-ckeditor--toolbar-fullscreen-button" data-role="ibo-vendors-ckeditor--toolbar-fullscreen-button" title="$sToggleFullScreen" style="background-image:url(\\'$sAbsoluteUrlAppRoot/images/full-screen.png\\')">&nbsp;</span>');
$('#'+oEditor.id+'_toolbox').append('<span class="ibo-vendors-ckeditor--toolbar-fullscreen-button editor-fullscreen-button" data-role="ibo-vendors-ckeditor--toolbar-fullscreen-button" title="$sToggleFullScreen">&nbsp;</span>');
$('#'+oEditor.id+'_toolbox .ibo-vendors-ckeditor--toolbar-fullscreen-button').on('click', function() {
oEditor.execCommand('maximize');
if ($(this).closest('.cke_maximized').length != 0)

View File

@@ -545,6 +545,7 @@ class LogChannels
public const DEADLOCK = 'DeadLock';
public const INLINE_IMAGE = 'InlineImage';
public const PORTAL = 'portal';
public const CMDB_SOURCE = 'cmdbsource';
}
@@ -676,11 +677,22 @@ abstract class LogAPI
* @param string $sChannel
*
* @return string one of the LEVEL_* const value : the one configured it if exists, otherwise default log level for this channel
* Config can be done globally : `'log_level_min' => LogAPI::LEVEL_TRACE,`
* Or per channel : `'log_level_min' => ['InlineImage' => LogAPI::LEVEL_TRACE, 'UserRequest' => LogAPI::LEVEL_TRACE],`
* Config can be set :
* * globally : `'log_level_min' => LogAPI::LEVEL_TRACE,`
* * per channel :
* ```
* 'log_level_min' => [
* '' => LogAPI::LEVEL_ERROR, // default log level for channels not listed below
* 'InlineImage' => LogAPI::LEVEL_TRACE,
* 'UserRequest' => LogAPI::LEVEL_TRACE
* ],
* ```
*
* @uses \LogAPI::GetConfig()
* @uses `log_level_min` config parameter
* @uses \LogAPI::GetLevelDefault
*
* @link https://www.itophub.io/wiki/page?id=3_0_0%3Aadmin%3Alog iTop log reference
*/
protected static function GetMinLogLevel($sChannel)
{
@@ -704,7 +716,12 @@ abstract class LogAPI
}
if (isset($sLogLevelMin[static::CHANNEL_DEFAULT])) {
return $sLogLevelMin[$sChannel];
return $sLogLevelMin[static::CHANNEL_DEFAULT];
}
// Even though the *self*::CHANNEL_DEFAULT is set to '' in the current class (LogAPI), the test below is necessary as the CHANNEL_DEFAULT constant can be (and is!) overloaded in derivated classes, don't remove this test to factorize it with the previous one.
if (isset($sLogLevelMin[''])) {
return $sLogLevelMin[''];
}
return static::GetLevelDefault();
@@ -824,6 +841,7 @@ class DeadLockLog extends LogAPI
class DeprecatedCallsLog extends LogAPI
{
public const ENUM_CHANNEL_PHP_METHOD = 'deprecated-php-method';
public const ENUM_CHANNEL_PHP_LIBMETHOD = 'deprecated-php-libmethod';
public const ENUM_CHANNEL_FILE = 'deprecated-file';
public const CHANNEL_DEFAULT = self::ENUM_CHANNEL_PHP_METHOD;
@@ -832,12 +850,79 @@ class DeprecatedCallsLog extends LogAPI
/** @var \FileLog we want our own instance ! */
protected static $m_oFileLog = null;
/**
* @param string|null $sTargetFile
*
*@uses \set_error_handler() to catch deprecated notices
*
* @since 3.0.0 N°3002 logs deprecated notices in called code
*/
public static function Enable($sTargetFile = null): void
{
if (empty($sTargetFile)) {
$sTargetFile = APPROOT.'log/deprecated-calls.log';
}
parent::Enable($sTargetFile);
try {
$bIsLogLevelEnabled = static::IsLogLevelEnabled(self::LEVEL_WARNING, self::ENUM_CHANNEL_PHP_LIBMETHOD);
}
catch (ConfigException $e) {
$bIsLogLevelEnabled = false;
}
if ($bIsLogLevelEnabled) {
set_error_handler([static::class, 'DeprecatedNoticesErrorHandler']);
}
}
/**
* This will catch a message for all E_DEPRECATED and E_USER_DEPRECATED errors.
* This handler is set in DeprecatedCallsLog::Enable
*
* @param int $errno
* @param string $errstr
* @param string $errfile
* @param int $errline
*
* @return bool
* @since 3.0.0 N°3002
* @noinspection SpellCheckingInspection
*/
public static function DeprecatedNoticesErrorHandler(int $errno, string $errstr, string $errfile, int $errline): bool
{
if (
(\E_USER_DEPRECATED !== $errno)
&& (\E_DEPRECATED !== $errno)
) {
return false;
}
$aStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4);
$iStackDeprecatedMethodLevel = 2; // level 0 = current method, level 1 = @trigger_error, level 2 = method containing the `trigger_error` call
$sDeprecatedObject = $aStack[$iStackDeprecatedMethodLevel]['class'];
$sDeprecatedMethod = $aStack[$iStackDeprecatedMethodLevel]['function'];
if (($sDeprecatedObject === __CLASS__) && ($sDeprecatedMethod === 'Log')) {
// We are generating a trigger_error ourselves, we don't want to trace them !
return false;
}
$sCallerFile = $aStack[$iStackDeprecatedMethodLevel]['file'];
$sCallerLine = $aStack[$iStackDeprecatedMethodLevel]['line'];
$sMessage = "Call to {$sDeprecatedObject}::{$sDeprecatedMethod} in {$sCallerFile}#L{$sCallerLine}";
$iStackCallerMethodLevel = $iStackDeprecatedMethodLevel + 1; // level 3 = caller of the deprecated method
if (array_key_exists($iStackCallerMethodLevel, $aStack)) {
$sCallerObject = $aStack[3]['class'];
$sCallerMethod = $aStack[3]['function'];
$sMessage .= " ({$sCallerObject}::{$sCallerMethod})";
}
if (!empty($errstr)) {
$sMessage .= ' : '.$errstr;
}
static::Warning($sMessage, self::ENUM_CHANNEL_PHP_LIBMETHOD);
return true;
}
protected static function GetLevelDefault(): string
@@ -897,16 +982,17 @@ class DeprecatedCallsLog extends LogAPI
}
$aStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
$sDeprecatedObject = $aStack[1]['class'];
$sDeprecatedMethod = $aStack[1]['function'];
$sCallerFile = $aStack[1]['file'];
$sCallerLine = $aStack[1]['line'];
$iStackDeprecatedMethodLevel = 1; // level 0 = current method, level 1 = method containing the `NotifyDeprecatedPhpMethod` call
$sDeprecatedObject = $aStack[$iStackDeprecatedMethodLevel]['class'];
$sDeprecatedMethod = $aStack[$iStackDeprecatedMethodLevel]['function'];
$sCallerFile = $aStack[$iStackDeprecatedMethodLevel]['file'];
$sCallerLine = $aStack[$iStackDeprecatedMethodLevel]['line'];
$sMessage = "Call to {$sDeprecatedObject}::{$sDeprecatedMethod} in {$sCallerFile}#L{$sCallerLine}";
if (array_key_exists(2, $aStack)) {
$sCallerObject = $aStack[2]['class'];
$sCallerMethod = $aStack[2]['function'];
$iStackCallerMethodLevel = $iStackDeprecatedMethodLevel + 1; // level 2 = caller of the deprecated method
if (array_key_exists($iStackCallerMethodLevel, $aStack)) {
$sCallerObject = $aStack[$iStackCallerMethodLevel]['class'];
$sCallerMethod = $aStack[$iStackCallerMethodLevel]['function'];
$sMessage .= " ({$sCallerObject}::{$sCallerMethod})";
}
@@ -914,12 +1000,12 @@ class DeprecatedCallsLog extends LogAPI
$sMessage .= ' : '.$sAdditionalMessage;
}
static::Warning($sMessage, static::ENUM_CHANNEL_PHP_METHOD);
static::Warning($sMessage, self::ENUM_CHANNEL_PHP_METHOD);
}
public static function Log($sLevel, $sMessage, $sChannel = null, $aContext = array()): void
{
if (utils::IsDevelopmentEnvironment()) {
if (true === utils::IsDevelopmentEnvironment()) {
trigger_error($sMessage, E_USER_DEPRECATED);
}

View File

@@ -1116,7 +1116,6 @@ abstract class MetaModel
*/
final public static function GetFilterCodeOrigin($sClass, $sAttCode)
{
DeprecatedCallsLog::NotifyDeprecatedPhpMethod('do not use : dead code, will be removed in the future');
self::_check_subclass($sClass);
return self::$m_aFilterOrigins[$sClass][$sAttCode];
@@ -6957,7 +6956,7 @@ abstract class MetaModel
* @param int $iKey id value of the object to retrieve
* @param bool $bMustBeFound see throws ArchivedObjectException
* @param bool $bAllowAllData if true then user rights will be bypassed - use with care!
* @param null $aModifierProperties
* @param array $aModifierProperties properties for {@see iQueryModifier} impl
*
* @return \DBObject null if : (the object is not found) or (archive mode disabled and object is archived and
* $bMustBeFound=false)
@@ -6977,12 +6976,9 @@ abstract class MetaModel
if (!utils::IsArchiveMode() && $oObject->IsArchived())
{
if ($bMustBeFound)
{
if ($bMustBeFound) {
throw new ArchivedObjectException("The object $sClass::$iKey is archived");
}
else
{
} else {
return null;
}
}
@@ -6990,6 +6986,35 @@ abstract class MetaModel
return $oObject;
}
/**
* @param string $sClass
* @param int $iKey
*
* @return bool True if the object of $sClass and $iKey exists in the DB -no matter the current user restrictions-, false otherwise meaning:
* - It could be in memory for now and is not persisted yet
* - It is neither in memory nor DB
*
* @throws \CoreException
* @throws \MySQLException
* @throws \MySQLQueryHasNoResultException
* @since 3.0.0 N°4173
*/
public static function IsObjectInDB(string $sClass, int $iKey): bool
{
// Note: We take the root class to ensure that there is a corresponding table in the DB
// as some intermediate classes can have no table in the DB.
$sRootClass = MetaModel::GetRootClass($sClass);
$sTable = MetaModel::DBGetTable($sRootClass);
$sKeyCol = MetaModel::DBGetKey($sRootClass);
$sEscapedKey = CMDBSource::Quote($iKey);
$sQuery = "SELECT count(*) FROM `{$sTable}` WHERE `{$sKeyCol}` = {$sEscapedKey}";
$iCount = (int) CMDBSource::QueryToScalar($sQuery);
return $iCount === 1;
}
/**
* Search for the specified class and id. If the object is archived it will be returned anyway (this is for pre-2.4
* module compatibility, see N.1108)
@@ -7244,7 +7269,6 @@ abstract class MetaModel
*/
public static function BulkUpdate(DBObjectSearch $oFilter, array $aValues)
{
DeprecatedCallsLog::NotifyDeprecatedPhpMethod('do not use : dead code, will be removed in the future');
// $aValues is an array of $sAttCode => $value
$sSQL = $oFilter->MakeUpdateQuery($aValues);
if (!self::DBIsReadOnly()) {

View File

@@ -504,21 +504,38 @@ class ormCaseLog {
$sHtml .= '</td></tr></table>';
return $sHtml;
}
/**
* Add a new entry to the log or merge the given text into the currently modified entry
* Add a new entry to the log or merge the given text into the currently modified entry
* and updates the internal index
* @param $sText string The text of the new entry
*
* @param string $sText The text of the new entry
* @param string $sOnBehalfOf Display this name instead of current user name
* @param null|int $iOnBehalfOfId Use this UserId to author this Entry. If $sOnBehalfOf equals '', it'll be replaced by this User friendlyname
*
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \OQLException
*
* @since 3.0.0 New $iOnBehalfOfId parameter
* @since 3.0.0 May throw \ArchivedObjectException exception
*/
public function AddLogEntry($sText, $sOnBehalfOf = '')
public function AddLogEntry(string $sText, $sOnBehalfOf = '', $iOnBehalfOfId = null)
{
$sText = HTMLSanitizer::Sanitize($sText);
$sDate = date(AttributeDateTime::GetInternalFormat());
if ($sOnBehalfOf == '')
{
if ($sOnBehalfOf == '' && $iOnBehalfOfId === null) {
$sOnBehalfOf = UserRights::GetUserFriendlyName();
$iUserId = UserRights::GetUserId();
}
elseif ($iOnBehalfOfId !== null) {
$iUserId = $iOnBehalfOfId;
/* @var User $oUser */
$oUser = MetaModel::GetObject('User', $iUserId, false, true);
if ($oUser !== null && $sOnBehalfOf === '') {
$sOnBehalfOf = $oUser->GetFriendlyName();
}
}
else
{
$iUserId = null;

View File

@@ -388,15 +388,18 @@ class SQLObjectQuery extends SQLQuery
{
if (count($this->__aSelectedIdFields) > 0)
{
$aCountFields = array();
foreach ($this->__aSelectedIdFields as $sFieldExpr)
{
$aCountFields[] = "COALESCE($sFieldExpr, 0)"; // Null values are excluded from the count
$aCountFields = [];
$aCountI = [];
$i = 0;
foreach ($this->__aSelectedIdFields as $sFieldExpr) {
$aCountFields[] = "COALESCE($sFieldExpr, 0) AS idCount$i"; // Null values are excluded from the count
$aCountI[] = 'idCount'.$i++;
}
$sCountFields = implode(', ', $aCountFields);
$sCountI = implode('+ ', $aCountI);
// Count can be limited for performance reason, in this case the total amount is not important,
// we only need to know if the number of entries is greater than a certain amount.
$sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep DISTINCT $sCountFields $sLineSep FROM $sFrom$sLineSep WHERE $sWhere $sLimit) AS _alderaan_";
$sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep DISTINCT $sCountFields $sLineSep FROM $sFrom$sLineSep WHERE $sWhere $sLimit) AS _alderaan_ WHERE $sCountI>0";
}
else
{

View File

@@ -333,77 +333,135 @@ abstract class User extends cmdbAbstractObject
{
parent::DoCheckToWrite();
// Note: This MUST be factorized later: declare unique keys (set of columns) in the data model
$oAddon = UserRights::GetModuleInstance();
$aChanges = $this->ListChanges();
if (array_key_exists('login', $aChanges))
{
if (strcasecmp($this->Get('login'), $this->GetOriginal('login')) !== 0)
{
if (array_key_exists('login', $aChanges)) {
// Check login uniqueness
if (strcasecmp($this->Get('login'), $this->GetOriginal('login')) !== 0) {
$sNewLogin = $aChanges['login'];
$oSearch = DBObjectSearch::FromOQL_AllData("SELECT User WHERE login = :newlogin");
if (!$this->IsNew())
{
if (!$this->IsNew()) {
$oSearch->AddCondition('id', $this->GetKey(), '!=');
}
$oSet = new DBObjectSet($oSearch, array(), array('newlogin' => $sNewLogin));
if ($oSet->Count() > 0)
{
if ($oSet->Count() > 0) {
$this->m_aCheckIssues[] = Dict::Format('Class:User/Error:LoginMustBeUnique', $sNewLogin);
}
}
}
// Check that this user has at least one profile assigned when profiles have changed
if (array_key_exists('profile_list', $aChanges))
{
$oSet = $this->Get('profile_list');
if ($oSet->Count() == 0)
{
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:AtLeastOneProfileIsNeeded');
// A User cannot disable himself
if ($this->IsCurrentUser()) {
if (isset($aChanges['status']) && ($this->Get('status') == 'disabled')) {
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:StatusChangeIsNotAllowed');
}
}
// Check that this user has at least one profile assigned when profiles have changed
if (array_key_exists('profile_list', $aChanges)) {
/** @var \DBObjectSet $oSet */
$oSet = $this->Get('profile_list');
if ($oSet->Count() == 0) {
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:AtLeastOneProfileIsNeeded');
}
// A user cannot add to themself a profile denying the access to the backoffice
$aForbiddenProfiles = PortalDispatcherData::GetData('backoffice')['deny'];
if ($this->IsCurrentUser()) {
$oSet->Rewind();
$aProfiles = [];
while ($oUserProfile = $oSet->Fetch()) {
$sProfile = $oUserProfile->Get('profile');
if (in_array($sProfile, $aForbiddenProfiles)) {
$this->m_aCheckIssues[] = Dict::Format('Class:User/Error:ProfileNotAllowed', $sProfile);
}
$aProfiles[$oUserProfile->Get('profileid')] = $sProfile;
}
if (!in_array(ADMIN_PROFILE_NAME, $aProfiles)) {
// Check if the user is yet allowed to modify Users
if (method_exists($oAddon, 'ResetCache')) {
$aCurrentProfiles = $_SESSION['profile_list'] ?? null;
// Set the current profiles into a session variable (not yet in the database)
$_SESSION['profile_list'] = $aProfiles;
$oAddon->ResetCache();
if (!$oAddon->IsActionAllowed($this, 'User', UR_ACTION_MODIFY, null)) {
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:CurrentProfilesHaveInsufficientRights');
}
$oAddon->ResetCache();
if (is_null($aCurrentProfiles)) {
unset($_SESSION['profile_list']);
} else {
$_SESSION['profile_list'] = $aCurrentProfiles;
}
}
}
}
}
// Only administrators can manage administrators
if (UserRights::IsAdministrator($this) && !UserRights::IsAdministrator())
{
if (UserRights::IsAdministrator($this) && !UserRights::IsAdministrator()) {
$this->m_aCheckIssues[] = Dict::S('UI:Login:Error:AccessRestricted');
}
if (!UserRights::IsAdministrator())
{
$oUser = UserRights::GetUserObject();
$oAddon = UserRights::GetModuleInstance();
if (!is_null($oUser) && method_exists($oAddon, 'GetUserOrgs'))
{
if ((empty($this->GetOriginal('contactid')) && !($this->IsNew())) || empty($this->Get('contactid')))
{
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:PersonIsMandatory');
// A contact is mandatory (an administrator can bypass it but not for himself)
if ((!UserRights::IsAdministrator() || $this->IsCurrentUser())
&& !$this->IsNew()
&& isset($aChanges['contactid'])
&& empty($this->Get('contactid'))) {
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:PersonIsMandatory');
}
// Allowed orgs must contains the user org (if any)
if (!empty($this->Get('org_id')) && !UserRights::IsAdministrator($this)) {
// Get the user org and all its parent orgs
$aUserOrgs = [$this->Get('org_id')];
$sHierarchicalKeyCode = MetaModel::IsHierarchicalClass('Organization');
if ($sHierarchicalKeyCode !== false) {
$sOrgQuery = 'SELECT Org FROM Organization AS Org JOIN Organization AS Root ON Org.'.$sHierarchicalKeyCode.' ABOVE Root.id WHERE Root.id = :id';
$oOrgSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData($sOrgQuery), [], ['id' => $this->Get('org_id')]);
while ($aRow = $oOrgSet->FetchAssoc()) {
$oOrg = $aRow['Org'];
$aUserOrgs[] = $oOrg->GetKey();
}
else
{
$aOrgs = $oAddon->GetUserOrgs($oUser, '');
if (count($aOrgs) > 0)
{
// Check that the modified User belongs to one of our organization
if (!in_array($this->GetOriginal('org_id'), $aOrgs) && !in_array($this->Get('org_id'), $aOrgs))
{
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:UserOrganizationNotAllowed');
}
// Check users with restricted organizations when allowed organizations have changed
if ($this->IsNew() || array_key_exists('allowed_org_list', $aChanges))
{
$oSet = $this->get('allowed_org_list');
if ($oSet->Count() == 0)
{
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:AtLeastOneOrganizationIsNeeded');
}
else
{
$aModifiedLinks = $oSet->ListModifiedLinks();
foreach ($aModifiedLinks as $oLink)
{
if (!in_array($oLink->Get('allowed_org_id'), $aOrgs))
{
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:OrganizationNotAllowed');
}
}
// Check the allowed orgs list
$oSet = $this->get('allowed_org_list');
if ($oSet->Count() > 0) {
$bFound = false;
while ($oOrg = $oSet->Fetch()) {
if (in_array($oOrg->Get('allowed_org_id'), $aUserOrgs)) {
$bFound = true;
break;
}
}
if (!$bFound) {
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:AllowedOrgsMustContainUserOrg');
}
}
}
if (!UserRights::IsAdministrator()) {
$oUser = UserRights::GetUserObject();
if (!is_null($oUser) && method_exists($oAddon, 'GetUserOrgs')) {
$aOrgs = $oAddon->GetUserOrgs($oUser, ''); // Modifier allowed orgs
if (count($aOrgs) > 0) {
// Check that the modified User belongs to one of our organization
if (!in_array($this->GetOriginal('org_id'), $aOrgs) && !in_array($this->Get('org_id'), $aOrgs)) {
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:UserOrganizationNotAllowed');
}
// Check users with restricted organizations when allowed organizations have changed
if ($this->IsNew() || array_key_exists('allowed_org_list', $aChanges)) {
$oSet = $this->get('allowed_org_list');
if ($oSet->Count() == 0) {
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:AtLeastOneOrganizationIsNeeded');
} else {
$aModifiedLinks = $oSet->ListModifiedLinks();
foreach ($aModifiedLinks as $oLink) {
if (!in_array($oLink->Get('allowed_org_id'), $aOrgs)) {
$this->m_aCheckIssues[] = Dict::S('Class:User/Error:OrganizationNotAllowed');
}
}
}
@@ -413,14 +471,26 @@ abstract class User extends cmdbAbstractObject
}
}
/**
* @inheritDoc
* @since 3.0.0
*/
public function DoCheckToDelete(&$oDeletionPlan)
{
parent::DoCheckToDelete($oDeletionPlan);
// A user cannot suppress himself
if ($this->IsCurrentUser()) {
$this->m_bSecurityIssue = true;
$this->m_aDeleteIssues[] = Dict::S('UI:Delete:NotAllowedToDelete');
}
}
function GetGrantAsHtml($sClass, $iAction)
{
if (UserRights::IsActionAllowed($sClass, $iAction, null, $this))
{
if (UserRights::IsActionAllowed($sClass, $iAction, null, $this)) {
return '<span style="background-color: #ddffdd;">'.Dict::S('UI:UserManagement:ActionAllowed:Yes').'</span>';
}
else
{
} else {
return '<span style="background-color: #ffdddd;">'.Dict::S('UI:UserManagement:ActionAllowed:No').'</span>';
}
}
@@ -528,6 +598,19 @@ abstract class User extends cmdbAbstractObject
}
parent::DBDeleteSingleObject();
}
/**
* @return bool
* @throws \OQLException
* @since 3.0.0
*/
protected function IsCurrentUser(): bool
{
if (is_null(UserRights::GetUserId())) {
return false;
}
return UserRights::GetUserId() == $this->GetKey();
}
}
/**
@@ -1011,6 +1094,30 @@ class UserRights
}
}
/**
* @param Person $oPerson Person we try to match against Users contact (also Person objects)
* @param bool $bMustBeUnique If true, return null when 2+ Users matching this Person were found. Otherwise return the first one
*
* @return \DBObject|null
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @since 3.0.0
*/
public static function GetUserFromPerson(Person $oPerson, bool $bMustBeUnique = true): ?DBObject
{
$sUserSearch = 'SELECT User WHERE contactid = :id';
$oUserSearch = DBObjectSearch::FromOQL($sUserSearch);
$oUserSearch->AllowAllData();
$oUserSet = new DBObjectSet($oUserSearch, array(), array('id' => $oPerson->GetKey()));
if($oUserSet->Count() > 0 && !($oUserSet->Count() > 1 && $bMustBeUnique)){
return $oUserSet->Fetch();
}
return null;
}
/**
* @return string
*/
@@ -1110,13 +1217,15 @@ class UserRights
}
}
}
} // If no contact, check if user has a placeholder in they preferences
else {
}
// If no contact & empty login, check if current user has a placeholder in they preferences
elseif ('' === $sLogin) {
$sPlaceholderPictureFilename = appUserPreferences::GetPref($sUserPicturePlaceholderPrefKey, null, static::GetUserId($sLogin));
if (!empty($sPlaceholderPictureFilename)) {
$sPictureUrl = utils::GetAbsoluteUrlAppRoot().$sUserPicturesFolder.$sPlaceholderPictureFilename;
}
}
// Else, no contact and no login, then it's for an unknown origin (system, extension, ...)
// Update cache
static::$m_aCacheContactPictureAbsUrl[$sLogin] = $sPictureUrl;

View File

@@ -4,3 +4,4 @@
*/
@import "block-csv";
@import "block-list";

View File

@@ -8,10 +8,4 @@ $ibo-block-csv--textarea--margin-top: 10px !default;
min-height: $ibo-block-csv--textarea--min-height;
margin-top: $ibo-block-csv--textarea--margin-top;
}
}
.ibo-block-csv--download-link{
@extend .ibo-button;
@extend .ibo-is-alternative;
@extend .ibo-is-primary;
}

View File

@@ -0,0 +1,14 @@
/*!
* @copyright Copyright (C) 2010-2021 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
$ibo-block-list--create-icon--margin-right: 0.5rem !default;
.ibo-block-list--empty-text, .ibo-block-list--create-action{
text-align: center;
}
.ibo-block-list--create-icon {
margin-right: $ibo-block-list--create-icon--margin-right;
}

View File

@@ -17,6 +17,7 @@
*/
$ibo-scrollbar--scrollbar-width: 8px !default;
$ibo-scrollbar--scrollbar-height: $ibo-scrollbar--scrollbar-width !default; /* For horizontal scrollbars */
$ibo-scrollbar--scrollbar-track-background-color: $ibo-color-transparent !default;
$ibo-scrollbar--scrollbar-thumb-background-color: $ibo-color-grey-300 !default;
$ibo-scrollbar--scrollbar-thumb-border: none !default;
@@ -34,6 +35,7 @@ $ibo-content-block--border: 1px solid $ibo-color-grey-400 !default;
/* CSS variables */
:root{
--ibo-scrollbar--scrollbar-width: #{$ibo-scrollbar--scrollbar-width};
--ibo-scrollbar--scrollbar-height: #{$ibo-scrollbar--scrollbar-height};
--ibo-scrollbar--scrollbar-track-background-color: #{$ibo-scrollbar--scrollbar-track-background-color};
--ibo-scrollbar--scrollbar-thumb-background-color: #{$ibo-scrollbar--scrollbar-thumb-background-color};
--ibo-scrollbar--scrollbar-thumb-border: #{$ibo-scrollbar--scrollbar-thumb-border};
@@ -60,6 +62,7 @@ $ibo-content-block--border: 1px solid $ibo-color-grey-400 !default;
/* - For Chrome/Edge/Safari */
&::-webkit-scrollbar {
width: var(--ibo-scrollbar--scrollbar-width);
height: var(--ibo-scrollbar--scrollbar-height);
}
&::-webkit-scrollbar-track {
background-color: var(--ibo-scrollbar--scrollbar-track-background-color);

View File

@@ -1,7 +1,4 @@
.ibo-blocklist--empty-text, .ibo-blocklist--create-new{
text-align: center;
}
.ibo-blocklist--medallion{
.ibo-block-list--medallion{
flex-direction: column;
align-items: center;
> .ibo-medallion-icon--image{

View File

@@ -2,6 +2,7 @@
* @copyright Copyright (C) 2010-2021 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
$ibo-panel-within-main-content--margin-bottom: 200px !default;
$ibo-panel-within-main-content--sticky-sentinel-top--top: -1 * $ibo-main-content--padding-top !default;
$ibo-panel-within-main-content--sticky-sentinel-top--height: $ibo-main-content--padding-top !default;
@@ -17,6 +18,8 @@ $ibo-panel-within-main-content--header--top--is-sticky: -1 * $ibo-main-content--
* - Unlike in JS, there no easy way to find the closest descendant
*/
.ibo-panel.ibo-has-sticky-header {
margin-bottom: $ibo-panel-within-main-content--margin-bottom; /* Add a margin below the panel so the dropdown lists can open without problem (N°4039) */
/* Stickable header rules */
> .ibo-sticky-sentinel-top {
top: $ibo-panel-within-main-content--sticky-sentinel-top--top;

View File

@@ -7,13 +7,13 @@
@import "button";
@import "button-group";
@import "breadcrumbs";
@import "collapsible-section";
@import "quick-create";
@import "global-search";
@import "popover-menu/popover-menu";
@import "popover-menu/popover-menu-item";
@import "newsroom-menu";
@import "panel";
@import "collapsible-section";
@import "modal";
@import "dashlet/all";
@import "input/all";

View File

@@ -17,21 +17,21 @@
*/
/* SCSS variables */
$ibo-collapsible-section--margin-top: 3rem;
$ibo-collapsible-section--title--color: $ibo-color-grey-900 !default;
$ibo-collapsible-section--body--background-color: $ibo-color-white-100 !default;
$ibo-collapsible-section--margin-top: 3rem !default;
$ibo-collapsible-section--title--color: $ibo-panel--title--color !default;
$ibo-collapsible-section--body--background-color: $ibo-panel--body--background-color !default;
$ibo-collapsible-section--highlight--height: 8px !default;
$ibo-collapsible-section--maximize-minimize-button--right: 5px !default;
$ibo-collapsible-section--maximize-minimize-button--color: $ibo-panel--collapsible-toggler--color !default;
$ibo-collapsible-section--maximize-minimize-button--right: $ibo-panel--collapsible-toggler--margin-right !default;
$ibo-collapsible-section--body--padding-bottom: 16px !default;
$ibo-collapsible-section--body--padding-top: $ibo-collapsible-section--body--padding-bottom + $ibo-collapsible-section--highlight--height !default;
$ibo-collapsible-section--body--padding-x: 16px !default;
$ibo-collapsible-section--body--border-radius: $ibo-border-radius-500 !default;
$ibo-collapsible-section--body--border-radius: $ibo-panel--body--border-radius !default;
$ibo-collapsible-section--body--border-size: 1px !default;
$ibo-collapsible-section--body--border-color: $ibo-color-grey-400 !default;
$ibo-collapsible-section--body--border-color: $ibo-panel--base-border-color !default;
/* Rules */
@@ -78,6 +78,7 @@ $ibo-collapsible-section--body--border-color: $ibo-color-grey-400 !default;
.ibo-collapsible-section--action-button {
&.ibo-collapsible-section--maximize-button, &.ibo-collapsible-section--minimize-button {
color: $ibo-collapsible-section--maximize-minimize-button--color;
margin-right: $ibo-collapsible-section--maximize-minimize-button--right;
}
}

View File

@@ -35,12 +35,12 @@ $ibo-field--value-decoration--spacing-x: 0.5rem !default;
/* Avoid value to overflow from its container with very long strings (typically URLs) */
/* Note: Some types of attribute must be excluding as it can alter their rendering */
&:not([data-attribute-type="AttributeBlob"]):not([data-attribute-type="AttributeFile"]):not([data-attribute-type="AttributeImage"]):not(.ibo-input-file-select--container) {
&:not([data-attribute-type="AttributeBlob"]):not([data-attribute-type="AttributeFile"]):not([data-attribute-type="AttributeImage"]):not([data-attribute-type="AttributeCustomFields"]):not(.ibo-input-file-select--container) {
/* We need the rule to apply for the class and all its descendants */
.ibo-field--value {
* {
word-break: break-word;
white-space: pre-wrap;
white-space: pre-line;
}
}
}
@@ -205,3 +205,7 @@ $ibo-field--value-decoration--spacing-x: 0.5rem !default;
.multi_values {
background-color: #c33;
}
.form_field ~ .form_field {
margin-top: $ibo-field--sibling-spacing;
}

View File

@@ -2,3 +2,7 @@
* copyright Copyright (C) 2010-2021 Combodo SARL
* license http://opensource.org/licenses/AGPL-3.0
*/
.ibo-prop-header {
@extend %ibo-font-size-150;
padding-bottom: 14px;
}

View File

@@ -22,7 +22,7 @@ $ibo-global-search--head--background-color: $ibo-color-white-100 !default;
$ibo-global-search--icon-padding-x: 16px !default;
$ibo-global-search--icon-padding-y: 0 !default;
$ibo-global-search--input--padding: 0 default;
$ibo-global-search--input--padding: 0 !default;
$ibo-global-search--input--padding-x--is-opened: 8px !default;
$ibo-global-search--input--padding-y--is-opened: 8px !default;
$ibo-global-search--input--width: 0 !default;
@@ -102,6 +102,7 @@ $ibo-global-search--compartment--placeholder-hint--text-color: $ibo-color-grey-7
padding: $ibo-global-search--input--padding;
width: $ibo-global-search--input--width;
color: $ibo-global-search--input--text-color;
background-color: transparent;
@extend %ibo-font-ral-nor-300;
border: none;

View File

@@ -237,26 +237,28 @@ $ibo-panel-colors: (
cursor: pointer;
}
.ibo-panel--collapsible-toggler--opened {
display: block;
}
.ibo-panel--collapsible-toggler--closed {
display: none;
}
/* Collapsible rules */
.ibo-panel:not(.ibo-is-opened) {
.ibo-panel--collapsible-toggler--closed {
.ibo-panel {
.ibo-panel--collapsible-toggler--opened {
display: block;
}
.ibo-panel--collapsible-toggler--opened {
.ibo-panel--collapsible-toggler--closed {
display: none;
}
&:not(.ibo-is-opened) {
.ibo-panel--collapsible-toggler--closed {
display: block;
}
.ibo-panel--body {
display: none;
.ibo-panel--collapsible-toggler--opened {
display: none;
}
.ibo-panel--body {
display: none;
}
}
}

View File

@@ -4,25 +4,43 @@ $ibo-prop--cancel--padding-left: 7px !default;
$ibo-prop--apply-cancel--span--height: 28px !default;
$ibo-prop--apply-cancel--span--width: 32px !default;
$ibo-prop--apply-cancel--height: $ibo-prop--apply-cancel--span--height !default;
$ibo-prop--apply--width: calc(#{$ibo-prop--apply-cancel--span--width} + #{$ibo-prop--apply--padding-left}) !default;
$ibo-prop--cancel--width: calc(#{$ibo-prop--apply-cancel--span--width} + #{$ibo-prop--cancel--padding-left}) !default;
$ibo-prop--apply--error--color: $ibo-color-grey-800 !default;
.ibo-prop--apply{
width: $ibo-prop--apply--width;
padding-left: $ibo-prop--apply--padding-left;
> span{
@extend .ibo-is-green;
}
&.ui-state-error{
&:after {
color: $ibo-prop--apply--error--color;
content: '\f071';
vertical-align: bottom;
@extend %fa-solid-base;
}
> span{
display: none !important;
}
}
}
.ibo-prop--cancel{
padding-left: $ibo-prop--cancel--padding-left;
width: $ibo-prop--cancel--width;
padding-left: $ibo-prop--cancel--padding-left;
> span{
@extend .ibo-is-red;
}
}
.ibo-prop--apply, .ibo-prop--cancel{
> span{
height: $ibo-prop--apply-cancel--height;
> span{
display: block;
height: $ibo-prop--apply-cancel--span--height;
width: $ibo-prop--apply-cancel--span--width;
text-align: center;
@extend .ibo-button;
@extend .ibo-is-alternative;
}
}

View File

@@ -9,6 +9,19 @@ $ibo-search-form-panel--body--padding-top: 18px !default;
$ibo-search-form-panel--body--padding-bottom: 10px !default;
$ibo-search-form-panel--body--padding-x: 14px !default;
$ibo-search-form-panel--criteria--color: $ibo-color-grey-900 !default;
$ibo-search-form-panel--criteria--background-color: $ibo-color-white-200 !default;
$ibo-search-form-panel--criteria--border-color: $ibo-color-grey-300 !default;
$ibo-search-form-panel--criteria--locked--background-color: $ibo-color-grey-300 !default;
$ibo-search-form-panel--more-criteria--color: $ibo-color-blue-grey-800 !default;
$ibo-search-form-panel--more-criteria--background-color: $ibo-color-white-100 !default;
$ibo-search-form-panel--more-criteria--icon--color: $ibo-color-primary-600 !default;
$ibo-search-form-panel--more-criteria--border-color: $ibo-search-form-panel--criteria--border-color !default;
$ibo-search-form-panel--misc-button--background-color: $ibo-search-form-panel--more-criteria--background-color !default;
$ibo-search-form-panel--misc-button--icon--color: $ibo-search-form-panel--more-criteria--icon--color !default;
$ibo-search-results-area--z-index: $ibo-search-form-panel--z-index - 2 !default; /* Minus 2 because the criteria expands between the search form panel and the results area */
$ibo-search-results-area--datatable-toolbar--background-color--is-sticking: $ibo-panel--body--background-color !default;
@@ -140,7 +153,6 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
.sf_criterion_area {
/*display: none;*/
padding: 8px 8px 3px 8px; /* padding-bottom must equals to padding-top - .search_form_criteria:margin-bottom */
background-color: $ibo-color-white-100;
.sf_criterion_row {
@@ -172,7 +184,7 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
display: inline;
.sfc_fg_button,
.sfc_header {
border: 1px solid #E1E7EC; /* Must be equal to .search_form_criteria:margin-bottom + this:padding-bottom */
border: 1px solid $ibo-search-form-panel--criteria--border-color; /* Must be equal to .search_form_criteria:margin-bottom + this:padding-bottom */
border-radius: 3px;
}
}
@@ -201,7 +213,7 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
> * {
padding: 7px 8px;
vertical-align: top;
border: solid 1px $ibo-color-grey-300;
border: solid 1px $ibo-search-form-panel--more-criteria--border-color;
border-radius: $ibo-border-radius-300;
}
@@ -229,7 +241,7 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
.search_form_criteria {
/* Non editable criteria */
&.locked {
background-color: $ibo-color-grey-200;
background-color: $ibo-search-form-panel--criteria--locked--background-color;
.sfc_title {
user-select: none;
@@ -278,8 +290,8 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
}
> * {
background-color: $ibo-color-white-200;
color: $ibo-color-grey-900;
background-color: $ibo-search-form-panel--criteria--background-color;
color: $ibo-search-form-panel--criteria--color;
}
/* Top left corner icons */
@@ -646,8 +658,8 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
}
> * {
background-color: $ibo-color-white-100;
color: $ibo-color-blue-grey-800;
background-color: $ibo-search-form-panel--more-criteria--background-color;
color: $ibo-search-form-panel--more-criteria--color;
}
.sfm_toggler {
@@ -702,8 +714,8 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
cursor: pointer;
> * {
background-color: $ibo-color-white-100;
color: $ibo-color-primary-600;
background-color: $ibo-search-form-panel--misc-button--background-color;
color: $ibo-search-form-panel--misc-button--icon--color;
}
}
}
@@ -835,9 +847,6 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
button {
margin-top: 8px;
@extend .ibo-button;
@extend .ibo-is-primary;
@extend .ibo-is-regular;
> span {
margin-right: 0.5em;
}
@@ -864,16 +873,6 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
padding-top: $ibo-panel--spacing-top ;
}
.sfc_fg_button{
@extend .ibo-button;
@extend .ibo-is-neutral;
&.sfc_fg_search, &.sfc_fg_apply, &.sfc_fg_cancel {
@extend .ibo-is-regular;
}
&.sfc_fg_more {
@extend .ibo-is-alternative;
}
}
.sfm_tg_title{
display: none;
}

View File

@@ -11,6 +11,7 @@ $ibo-dashlet--elements-spacing-y: 24px !default;
/* Rules */
.ibo-dashlet {
position: relative;
width: calc(#{$ibo-dashlet--width} - #{$ibo-dashlet--elements-spacing-x});
margin: calc(#{$ibo-dashlet--elements-spacing-y} / 2) calc(#{$ibo-dashlet--elements-spacing-x} / 2);
@@ -27,4 +28,13 @@ $ibo-dashlet--elements-spacing-y: 24px !default;
}
.ibo-details{
margin-top: 5px;
}
.ibo-dashlet-blocker{
position: absolute;
z-index: 9; /* To be above calendar links & all, but below .close-box (9) */
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: not-allowed;
}

View File

@@ -25,9 +25,7 @@ $ibo-input-datetime--ui-tpicker-slider--padding-right: 18px !default;
color: $ibo-input-datetime--action-button--color;
}
.ui-datepicker-current, .ui-datepicker-close{
@extend .ibo-button;
@extend .ibo-is-regular;
@extend .ibo-is-secondary;
@extend .ibo-button, .ibo-is-regular, .ibo-is-secondary;
}
.ui_tpicker_hour_slider, .ui_tpicker_minute_slider, .ui_tpicker_second_slider{
@extend .ibo-input-wrapper;

View File

@@ -35,10 +35,13 @@ $ibo-input-select--action-button--padding-left: 6px !default;
$ibo-input-select--action-button--padding-right: 2px !default;
.ibo-input-select {
appearance: none;
display: inline-block;
min-width: $ibo-input-select--value--min-midth;
&:not(.ibo-input-select-autocomplete):not(.ibo-input-selectize) {
appearance: none;
}
&.ibo-input-selectize {
padding-right: 0;
padding-left: 0;
@@ -56,6 +59,10 @@ $ibo-input-select--action-button--padding-right: 2px !default;
padding-left: $ibo-input--padding-x;
}
}
&[size]{
height: auto;
}
}
.ibo-input-select-autocomplete{
min-width: $ibo-input-select-autocomplete--value--min-midth !important;

View File

@@ -98,7 +98,7 @@ $ibo-navigation-menu--menu-group-title--text-color--is-active: $ibo-color-blue-g
$ibo-navigation-menu--drawer--width: 312px !default;
$ibo-navigation-menu--drawer--padding-x: 20px !default;
$ibo-navigation-menu--drawer--padding-y: 32px !default;
$ibo-navigation-menu--drawer--background-color: $ibo-color-grey-100 !default;
$ibo-navigation-menu--drawer--background-color: $ibo-navigation-menu--menu-group--background-color--is-active !default;
$ibo-navigation-menu--drawer--border-right: 1px solid $ibo-color-grey-300 !default;
/* TODO: Refactor this into the standard field input */

View File

@@ -56,12 +56,17 @@ $ibo-activity-entry--sub-information--margin-top: 4px !default;
$ibo-activity-entry--sub-information--margin-bottom: $ibo-activity-entry--sub-information--margin-top !default;
$ibo-activity-entry--sub-information--text-color: $ibo-color-grey-700 !default;
$ibo-activity-entry--author-name--sibling-spacing: 0.2rem !default;
$ibo-activity-entry--sub-information--sibling-spacing: 0.5rem !default;
$ibo-activity-entry--sub-information-sibling-separator--size: 4px !default;
$ibo-activity-entry--sub-information-sibling-separator--border-radius: 100% !default;
$ibo-activity-entry--sub-information-sibling-separator--background-color: $ibo-color-grey-600 !default;
$ibo-activity-panel--load-more-entries--size: 32px !default;
$ibo-activity-panel--load-more-entries--border-radius: $ibo-border-radius-full !default;
$ibo-activity-panel--load-more-entries--background-color: $ibo-activity-entry--main-information--background-color !default;
$ibo-activity-panel--load-more-entries--border: $ibo-content-block--border !default;
$ibo-activity-panel--load-entries-button--size: 32px !default;
$ibo-activity-panel--load-entries-button--border-radius: $ibo-border-radius-full !default;
$ibo-activity-panel--load-entries-button--background-color: $ibo-activity-entry--main-information--background-color !default;
$ibo-activity-panel--load-entries-button--border: $ibo-content-block--border !default;
$ibo-activity-panel--load-all-entries--is-hover--margin-left: ($ibo-activity-panel--load-entries-button--size + 10px) * 2 !default; /* 2x is necessary here as the elements are centered */
/* Entry group */
.ibo-activity-panel--entry-group{
@@ -198,7 +203,7 @@ $ibo-activity-panel--load-more-entries--border: $ibo-content-block--border !defa
/* Avoid pre (code snippets) to overflow outside the entry */
pre {
white-space: pre-wrap;
white-space: pre-line;
}
/* Avoid table to overflow outside the entry (see N°2127) */
@@ -228,26 +233,52 @@ $ibo-activity-panel--load-more-entries--border: $ibo-content-block--border !defa
color: $ibo-activity-entry--sub-information--text-color;
@extend %ibo-font-size-50;
}
.ibo-activity-entry--author-name {
&:after {
content: "-";
margin-left: $ibo-activity-entry--author-name--sibling-spacing;
margin-right: $ibo-activity-entry--author-name--sibling-spacing;
> *:not(:last-child):after {
content: " ";
display: inline-block;
vertical-align: middle;
margin-left: $ibo-activity-entry--sub-information--sibling-spacing;
margin-right: $ibo-activity-entry--sub-information--sibling-spacing;
width: $ibo-activity-entry--sub-information-sibling-separator--size;
height: $ibo-activity-entry--sub-information-sibling-separator--size;
border-radius: $ibo-activity-entry--sub-information-sibling-separator--border-radius;
background-color: $ibo-activity-entry--sub-information-sibling-separator--background-color;
}
}
.ibo-activity-panel--load-more-entries-container {
position: relative;
@extend %ibo-fully-centered-content;
&:hover {
.ibo-activity-panel--load-all-entries {
margin-left: $ibo-activity-panel--load-all-entries--is-hover--margin-left;
}
}
&:not(:hover) {
.ibo-activity-panel--load-all-entries {
visibility: hidden;
}
}
}
.ibo-activity-panel--load-more-entries {
width: $ibo-activity-panel--load-more-entries--size;
height: $ibo-activity-panel--load-more-entries--size;
border-radius: $ibo-activity-panel--load-more-entries--border-radius;
background-color: $ibo-activity-panel--load-more-entries--background-color;
border: $ibo-activity-panel--load-more-entries--border;
.ibo-activity-panel--load-entries-button {
width: $ibo-activity-panel--load-entries-button--size;
height: $ibo-activity-panel--load-entries-button--size;
border-radius: $ibo-activity-panel--load-entries-button--border-radius;
background-color: $ibo-activity-panel--load-entries-button--background-color;
border: $ibo-activity-panel--load-entries-button--border;
@extend %ibo-fully-centered-content;
@extend %ibo-hyperlink-inherited-colors;
}
.ibo-activity-panel--load-more-entries {
z-index: 1;
}
.ibo-activity-panel--load-all-entries {
position: absolute;
z-index: 0; /* Must be below the other button as it will reveal later */
top: 0;
margin-left: 0;
transition: all 0.1s ease-in-out;
}

View File

@@ -34,7 +34,9 @@ $ibo-activity-panel--togglers--elements-spacing: 0.75rem !default;
/* - Tabs togglers*/
$ibo-activity-panel--tabs-togglers--padding-x: $ibo-activity-panel--padding-x * 3 !default; /* We need to increase this so the size toggler which will be set in abs. pos. can overlap it nicely */
$ibo-activity-panel--tab-toolbar-action--color: $ibo-color-grey-900 !default;
$ibo-activity-panel--tab-toolbar-info--color: $ibo-activity-panel--tab-toolbar-action--color !default;
/* - Tab toggler */
$ibo-activity-panel--tab-toggler--caselog-highlight-colors: $ibo-caselog-highlight-colors !default;
$ibo-activity-panel--tab-toggler--is-active--background-color: $ibo-color-grey-200 !default;
@@ -278,7 +280,8 @@ $ibo-activity-panel--open-icon--margin-left: 0.75rem !default;
}
.ibo-activity-panel--tab-toolbar-right-actions {
.ibo-activity-panel--tab-toolbar-info {
> .ibo-activity-panel--tab-toolbar-info-icon {
color: $ibo-activity-panel--tab-toolbar-info--color;
> .ibo-activity-panel--tab-toolbar-info-icon {
margin-left: $ibo-activity-panel--tab-toolbar-info-icon--margin-left;
}
@@ -289,6 +292,7 @@ $ibo-activity-panel--open-icon--margin-left: 0.75rem !default;
}
.ibo-activity-panel--tab-toolbar-action{
position: relative;
color: $ibo-activity-panel--tab-toolbar-action--color;
@extend %ibo-fully-centered-content;
}
.ibo-activity-panel--filter{

View File

@@ -44,15 +44,17 @@ $ibo-dashboard-editor--delete-dashlet-icon--z-index: 21 !default;
flex-direction: column;
padding-bottom: 20px;
table{
width: 100%;
text-align: left;
td{
margin-bottom: 14px;
.ibo-field{
@extend %ibo-font-size-100;
}
}
}
width: 100%;
text-align: left;
td, th {
margin-bottom: 14px;
.ibo-field {
@extend %ibo-font-size-100;
}
}
}
}
.ibo-dashboard-editor--properties-title{
padding-bottom: $ibo-dashboard-editor--properties-title--padding-bottom;
@@ -98,7 +100,9 @@ $ibo-dashboard-editor--delete-dashlet-icon--z-index: 21 !default;
right: $ibo-dashboard-editor--delete-dashlet-icon--right;
padding: $ibo-dashboard-editor--delete-dashlet-icon--padding;
z-index: $ibo-dashboard-editor--delete-dashlet-icon--z-index;
@extend .ibo-button;
@extend .ibo-is-alternative;
@extend .ibo-is-danger;
}
.ibo-dashboard-editor .itop-dashboard{
a{
cursor: not-allowed;
}
}

View File

@@ -80,7 +80,10 @@ $ibo-display-graph--search-box--criterion--content--checkbox--margin-right: 10px
padding-right: 0;
}
.ibo-display-graph--search-box {
.sf_criterion_area{
display: flex;
flex-direction: column;
}
.sf_criterion_row {
display: flex;
flex-wrap: wrap;
@@ -103,6 +106,6 @@ $ibo-display-graph--search-box--criterion--content--checkbox--margin-right: 10px
}
#ReloadMovieBtn {
float: right;
align-self: flex-end;
}
}

View File

@@ -142,6 +142,19 @@ body.ibo-has-fullscreen-descendant {
@extend %ibo-font-code-150;
}
/*
* A single class to handle WYSIWYG generated content, where only HTML tags are available
* See https://bulma.io/documentation/elements/content/
*/
.ibo-is-html-content {
@extend .content;
code {
color: initial;
}
}
/***********************************************************************/
/* Sticky headers */
/* */

View File

@@ -19,6 +19,7 @@
@import "bulma-variables-overload";
@import "../../../node_modules/bulma-scss/bulma";
@import "ckeditor";
@import "tippy";
@import "jqueryui";
@import "jquery-multiselect";
@import "datatables";

View File

@@ -23,4 +23,15 @@ $body-font-size: $ibo-font-size-100 !default;
$body-weight: $ibo-font-weight-500 !default;
$body-overflow-x: hidden !default;
$body-overflow-y: auto !default;
$body-overflow-y: auto !default;
/**
* customize Bulma content variables
* See https://bulma.io/documentation/elements/content/
*/
$content-table-cell-border: 1px solid black;
$content-table-cell-border-width: 1px;
$content-table-head-cell-border-width: 1px;
$content-table-foot-cell-border-width: 1px;
$content-table-foot-cell-color: black;

View File

@@ -17,7 +17,7 @@ $ibo-vendors-ckeditor--autocomplete-panel--background-color: $ibo-color-white-10
$ibo-vendors-ckeditor--autocomplete-item-image--size: 25px !default;
$ibo-vendors-ckeditor--autocomplete-item-image--margin-right: 0.5rem !default;
$ibo-vendors-ckeditor--autocomplete-item-image--background-color: $ibo-color-grey-200 !default;
$ibo-vendors-ckeditor--autocomplete-item-image--background-color: $ibo-color-blue-100 !default;
$ibo-vendors-ckeditor--autocomplete-item-image--border: 1px solid $ibo-color-grey-600 !default;
$ibo-vendors-ckeditor--autocomplete-item-title--text-color: #3A3A3A !default;
@@ -37,6 +37,7 @@ $ibo-vendors-ckeditor--autocomplete-item-title--text-color: #3A3A3A !default;
background-position: center center !important;
background-repeat: no-repeat !important;
background-size: 100% !important;
background-image: url('../../../../images/full-screen.png') !important;
&:hover {
background-color: $ibo-vendors-ckeditor--toolbar-fullscreen-button--background-color--on-hover;
@@ -48,7 +49,7 @@ $ibo-vendors-ckeditor--autocomplete-item-title--text-color: #3A3A3A !default;
padding: $ibo-vendors-highlightjs--padding !important;
box-shadow: 0 0px 3px 2px inset rgba(0, 0, 0, 0.4);
border-radius: $ibo-vendors-highlightjs--border-radius;
white-space: pre-wrap;
white-space: pre-line;
}
/* Mentions in caselogs */
@@ -67,6 +68,9 @@ ul.cke_autocomplete_panel{
.ibo-vendors-ckeditor--autocomplete-item-image{
width: $ibo-vendors-ckeditor--autocomplete-item-image--size;
height: $ibo-vendors-ckeditor--autocomplete-item-image--size;
/* min-xxx are here to avoid medallion to be horizontally compressed when the title is to long */
min-width: $ibo-vendors-ckeditor--autocomplete-item-image--size;
min-height: $ibo-vendors-ckeditor--autocomplete-item-image--size;
background-position: center center;
background-size: 100%;
border-radius: 100%;
@@ -77,6 +81,7 @@ ul.cke_autocomplete_panel{
@extend %ibo-fully-centered-content;
}
.ibo-vendors-ckeditor--autocomplete-item-title{
white-space: nowrap; /* Here we don't want to truncate the text as in an autocomplete we might have similar values and we need the user to see the entire text to be able to differenciate them */
color: $ibo-vendors-ckeditor--autocomplete-item-title--text-color;
@extend %ibo-font-weight-700;
}

View File

@@ -167,9 +167,7 @@ $ibo-vendors-jqueryui--ui-slider--ui-slider-handle--hover--border-color: $ibo-co
top: 0;
}
.ui-button {
@extend .ibo-button;
@extend .ibo-is-regular;
@extend .ibo-is-secondary;
@extend .ibo-button, .ibo-is-regular, .ibo-is-secondary;
> .ui-icon {
background-image: none;
float: unset;
@@ -301,7 +299,7 @@ $ibo-vendors-jqueryui--ui-slider--ui-slider-handle--hover--border-color: $ibo-co
background-color: $ibo-vendors-jqueryui--ui-datepicker--background-color;
border-radius: $ibo-vendors-jqueryui--ui-datepicker--border-radius;
box-shadow: $ibo-vendors-jqueryui--ui-datepicker--box-shadow;
z-index: 21 !important;
z-index: 32 !important;
padding: 0px 8px 5px 8px;
.ui-datepicker-header {
@@ -569,6 +567,9 @@ $ibo-vendors-jqueryui--ui-slider--ui-slider-handle--hover--border-color: $ibo-co
left: 0;
cursor: default;
z-index: 100;
.ui-menu-item{
padding: 0;
}
}
.ui-autocomplete-input{
@extend .ibo-input;

9
css/backoffice/vendors/_tippy.scss vendored Normal file
View File

@@ -0,0 +1,9 @@
/*!
* @copyright Copyright (C) 2010-2021 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
// Allow plain text (opposite of HTML) multi-lines tooltips to be displayed correctly, otherwise it will all be on a single line.
.tippy-content {
white-space: pre-line;
}

View File

@@ -1,5 +1,9 @@
@CHARSET "UTF-8";
*, ::before, ::after {
box-sizing: content-box;
}
html,body {
height: 100%;
}
@@ -107,10 +111,9 @@ a:hover {
}
#login-form-content .divider hr {
margin-left:auto;
margin-right:auto;
margin: 0.5rem auto;
width:40%;
color: #E1E7EC;
background-color: #E1E7EC;
}
#login-form-content .divider hr.left {

File diff suppressed because one or more lines are too long

View File

@@ -36,6 +36,8 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
'Class:UserLocal/Attribute:expiration/Value:never_expire+' => '',
'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+' => '',
'Class:UserLocal/Attribute:password_renewed_date' => 'Letzte Passworterneuerung',
'Class:UserLocal/Attribute:password_renewed_date+' => 'Letztes Änderungsdatum',

View File

@@ -49,10 +49,13 @@ Dict::Add('EN US', 'English', 'English', array(
'Class:UserLocal/Attribute:expiration/Value:never_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:force_expire' => 'Expired',
'Class:UserLocal/Attribute:expiration/Value:force_expire+' => '',
'Class:UserLocal/Attribute:password_renewed_date' => 'Password renewal',
'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' => 'Password renewed on',
'Class:UserLocal/Attribute:password_renewed_date+' => 'When the password was last changed',
'Error:UserLocalPasswordValidator:UserPasswordPolicyRegex:ValidationFailed' => 'Password must be at least 8 characters and include uppercase, lowercase, numeric and special characters.',
'UserLocal:password:expiration' => 'The fields below require an extension'
'UserLocal:password:expiration' => 'The fields below require an extension',
'Class:UserLocal/Error:OneTimePasswordChangeIsNotAllowed' => 'Setting password expiration to "One-time password" is not allowed for your own User',
));

View File

@@ -27,16 +27,19 @@ Dict::Add('FR FR', 'French', 'Français', array(
'Class:UserLocal/Attribute:expiration' => 'Validité du mot de passe',
'Class:UserLocal/Attribute:expiration+' => 'Statut du mot de passe (nécessite une extension pour avoir un effet)',
'Class:UserLocal/Attribute:expiration/Value:can_expire' => 'à durée limitée',
'Class:UserLocal/Attribute:expiration/Value:can_expire' => 'Durée limitée',
'Class:UserLocal/Attribute:expiration/Value:can_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:never_expire' => 'à validité permanente',
'Class:UserLocal/Attribute:expiration/Value:never_expire' => 'Permanente',
'Class:UserLocal/Attribute:expiration/Value:never_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:force_expire' => 'à changer',
'Class:UserLocal/Attribute:expiration/Value:force_expire' => 'A changer à la prochaine connexion',
'Class:UserLocal/Attribute:expiration/Value:force_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:otp_expire' => 'Usage unique',
'Class:UserLocal/Attribute:expiration/Value:otp_expire+' => '',
'Class:UserLocal/Attribute:password_renewed_date' => 'Mot de passe changé le',
'Class:UserLocal/Attribute:password_renewed_date+' => 'Dernière date à laquelle le mot de passe a été changé',
'Error:UserLocalPasswordValidator:UserPasswordPolicyRegex:ValidationFailed' => 'Le mot de passe doit contenir au moins 8 caractères, avec minuscule, majuscule, nombre et caractère spécial.',
'UserLocal:password:expiration' => 'Les champs ci-dessous nécessitent une extension'
'UserLocal:password:expiration' => 'Les champs ci-dessous nécessitent une extension',
'Class:UserLocal/Error:OneTimePasswordChangeIsNotAllowed' => 'Impossible de mettre "Usage unique" comme validité du mot de passe pour son propre utilisateur.',
));

View File

@@ -68,7 +68,8 @@ class UserLocal extends UserInternal
const EXPIRE_CAN = 'can_expire';
const EXPIRE_NEVER = 'never_expire';
const EXPIRE_FORCE = 'force_expire';
const EXPIRE_ONE_TIME_PWD = 'otp_expire';
/** @var UserLocalPasswordValidity|null */
protected $m_oPasswordValidity = null;
@@ -90,8 +91,8 @@ class UserLocal extends UserInternal
MetaModel::Init_AddAttribute(new AttributeOneWayPassword("password", array("allowed_values"=>null, "sql"=>"pwd", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
$sExpireEnum = implode(',', array(self::EXPIRE_CAN, self::EXPIRE_NEVER, self::EXPIRE_FORCE));
MetaModel::Init_AddAttribute(new AttributeEnum("expiration", array("allowed_values"=>new ValueSetEnum($sExpireEnum), "sql"=>"expiration", "default_value"=>'never_expire', "is_null_allowed"=>false, "depends_on"=>array())));
$sExpireEnum = implode(',', array(self::EXPIRE_CAN, self::EXPIRE_NEVER, self::EXPIRE_FORCE, self::EXPIRE_ONE_TIME_PWD));
MetaModel::Init_AddAttribute(new AttributeEnum("expiration", array("allowed_values"=>new ValueSetEnum($sExpireEnum), "sql"=>"expiration", "default_value"=>self::EXPIRE_NEVER, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeDate("password_renewed_date", array("allowed_values"=>null, "sql"=>"password_renewed_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
// Display lists
@@ -136,6 +137,10 @@ class UserLocal extends UserInternal
{
return false;
}
if($this->Get('expiration') == self::EXPIRE_ONE_TIME_PWD)
{
return false;
}
return true;
}
@@ -184,6 +189,8 @@ class UserLocal extends UserInternal
protected function OnInsert()
{
parent::OnInsert();
$sToday = date(\AttributeDate::GetInternalFormat());
$this->Set('password_renewed_date', $sToday);
$this->OnWrite();
}
@@ -202,6 +209,15 @@ class UserLocal extends UserInternal
$sNow = date(\AttributeDate::GetInternalFormat());
$this->Set('password_renewed_date', $sNow);
// Reset the "force" expiration flag when the user updates her/his own password!
if ($this->IsCurrentUser())
{
if (($this->Get('expiration') == self::EXPIRE_FORCE))
{
$this->Set('expiration', self::EXPIRE_CAN);
}
}
}
public function IsPasswordValid()
@@ -278,7 +294,13 @@ class UserLocal extends UserInternal
{
$this->m_aCheckIssues[] = $this->m_oPasswordValidity->getPasswordValidityMessage();
}
// A User cannot force a one-time password on herself/himself
if ($this->IsCurrentUser()) {
if (array_key_exists('expiration', $this->ListChanges()) && ($this->Get('expiration') == self::EXPIRE_ONE_TIME_PWD)) {
$this->m_aCheckIssues[] = Dict::S('Class:UserLocal/Error:OneTimePasswordChangeIsNotAllowed');
}
}
parent::DoCheckToWrite();
}

View File

@@ -27,6 +27,8 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', array(
'DBTools:Class' => 'Class~~',
'DBTools:Title' => 'Database Maintenance Tools~~',
'DBTools:ErrorsFound' => 'Errors Found~~',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => 'Error~~',
'DBTools:Count' => 'Count~~',
'DBTools:SQLquery' => 'SQL query~~',

View File

@@ -27,6 +27,8 @@ Dict::Add('DA DA', 'Danish', 'Dansk', array(
'DBTools:Class' => 'Class~~',
'DBTools:Title' => 'Database Maintenance Tools~~',
'DBTools:ErrorsFound' => 'Errors Found~~',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => 'Error~~',
'DBTools:Count' => 'Count~~',
'DBTools:SQLquery' => 'SQL query~~',

View File

@@ -24,6 +24,8 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
'DBTools:Class' => 'Klasse',
'DBTools:Title' => 'Datenbankpflege-Tools',
'DBTools:ErrorsFound' => 'Fehler gefunden',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => 'Fehler',
'DBTools:Count' => 'Anzahl',
'DBTools:SQLquery' => 'SQL Query',

View File

@@ -27,6 +27,8 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellaño', array(
'DBTools:Class' => 'Clase',
'DBTools:Title' => 'Herramientas de mantenimiento de base de datos~~',
'DBTools:ErrorsFound' => 'Errores encontrados',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => 'Error',
'DBTools:Count' => 'Cantidad',
'DBTools:SQLquery' => 'Consulta SQL',

View File

@@ -27,6 +27,8 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
'DBTools:Class' => 'Class~~',
'DBTools:Title' => 'Database Maintenance Tools~~',
'DBTools:ErrorsFound' => 'Errors Found~~',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => 'Error~~',
'DBTools:Count' => 'Count~~',
'DBTools:SQLquery' => 'SQL query~~',

View File

@@ -27,6 +27,8 @@ Dict::Add('IT IT', 'Italian', 'Italiano', array(
'DBTools:Class' => 'Class~~',
'DBTools:Title' => 'Database Maintenance Tools~~',
'DBTools:ErrorsFound' => 'Errors Found~~',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => 'Error~~',
'DBTools:Count' => 'Count~~',
'DBTools:SQLquery' => 'SQL query~~',

View File

@@ -27,6 +27,8 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
'DBTools:Class' => 'Class~~',
'DBTools:Title' => 'Database Maintenance Tools~~',
'DBTools:ErrorsFound' => 'Errors Found~~',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => 'Error~~',
'DBTools:Count' => 'Count~~',
'DBTools:SQLquery' => 'SQL query~~',

View File

@@ -29,6 +29,8 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
'DBTools:Class' => 'Klasse',
'DBTools:Title' => 'Onderhoudstools voor de database~~',
'DBTools:ErrorsFound' => 'Fouten gevonden',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => 'Fout',
'DBTools:Count' => 'Aantal',
'DBTools:SQLquery' => 'SQL-query',

View File

@@ -27,6 +27,8 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
'DBTools:Class' => 'Classe',
'DBTools:Title' => 'Manutenção da Base de Dados',
'DBTools:ErrorsFound' => 'Erros Encontrados',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => 'Erros',
'DBTools:Count' => 'Quantidade',
'DBTools:SQLquery' => 'Query SQL',

View File

@@ -14,6 +14,8 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
'DBTools:Class' => 'Класс',
'DBTools:Title' => 'Инструменты обслуживания базы данных~~',
'DBTools:ErrorsFound' => 'Найденные ошибки',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => 'Ошибка',
'DBTools:Count' => 'Количество',
'DBTools:SQLquery' => 'SQL-запрос',

View File

@@ -27,6 +27,8 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', array(
'DBTools:Class' => 'Class~~',
'DBTools:Title' => 'Database Maintenance Tools~~',
'DBTools:ErrorsFound' => 'Errors Found~~',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => 'Error~~',
'DBTools:Count' => 'Count~~',
'DBTools:SQLquery' => 'SQL query~~',

View File

@@ -27,6 +27,8 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
'DBTools:Class' => 'Class~~',
'DBTools:Title' => 'Database Maintenance Tools~~',
'DBTools:ErrorsFound' => 'Errors Found~~',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => 'Error~~',
'DBTools:Count' => 'Count~~',
'DBTools:SQLquery' => 'SQL query~~',

View File

@@ -27,6 +27,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'DBTools:Class' => 'Class~~',
'DBTools:Title' => '数据库维护工具',
'DBTools:ErrorsFound' => '发现错误',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Error' => '错误',
'DBTools:Count' => '个数',
'DBTools:SQLquery' => 'SQL 查询',

View File

@@ -32,8 +32,6 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Attachments:PreviewNotAvailable' => '该附件类型不支持预览.',
'Attachments:Error:FileTooLarge' => '上传的文件过大. %1$s',
'Attachments:Error:UploadedFileEmpty' => '收到的文件为空,无法添加. 可能是因为您发送的是空文件,或者咨询 iTop 管理员服务器磁盘是否已满. ',
'Attachments:Render:Icons' => '显示为图标',
'Attachments:Render:Table' => '显示为列表',
'UI:Attachments:DropYourFileHint' => '将文件拖放到此区域的任意位置',

View File

@@ -170,10 +170,6 @@ JS
require_once(dirname(__FILE__).'/dbrestore.class.inc.php');
$sEnvironment = utils::ReadParam('environment', 'production', false, 'raw_data');
$oRestoreMutex = new iTopMutex('restore.'.$sEnvironment);
IssueLog::Info("Backup Restore - Acquiring the LOCK 'restore.$sEnvironment'");
$oRestoreMutex->Lock();
IssueLog::Info('Backup Restore - LOCK acquired, executing...');
try
{
set_time_limit(0);
@@ -203,7 +199,6 @@ JS
finally
{
unlink($tokenRealPath);
$oRestoreMutex->Unlock();
}
$oPage->output();

View File

@@ -30,9 +30,6 @@ if (!defined('APPROOT'))
}
}
require_once(APPROOT.'application/application.inc.php');
require_once(APPROOT.'application/webpage.class.inc.php');
require_once(APPROOT.'application/csvpage.class.inc.php');
require_once(APPROOT.'application/clipage.class.inc.php');
require_once(APPROOT.'application/ajaxwebpage.class.inc.php');
require_once(APPROOT.'core/log.class.inc.php');

View File

@@ -28,8 +28,8 @@ class DBRestore extends DBBackup
{
parent::__construct($oConfig);
$this->sDBUser = $oConfig->Get('db_user');
$this->sDBPwd = $oConfig->Get('db_pwd');
$this->sDBUser = $this->oConfig->Get('db_user');
$this->sDBPwd = $this->oConfig->Get('db_pwd');
}
protected function LogInfo($sMsg)
@@ -127,79 +127,88 @@ class DBRestore extends DBBackup
*/
public function RestoreFromCompressedBackup($sFile, $sEnvironment = 'production')
{
$this->LogInfo("Starting restore of ".basename($sFile));
$oRestoreMutex = new iTopMutex('restore.'.$sEnvironment);
IssueLog::Info("Backup Restore - Acquiring the LOCK 'restore.$sEnvironment'");
$oRestoreMutex->Lock();
$sNormalizedFile = strtolower(basename($sFile));
if (substr($sNormalizedFile, -4) == '.zip')
{
$this->LogInfo('zip file detected');
$oArchive = new ZipArchiveEx();
$oArchive->open($sFile);
}
elseif (substr($sNormalizedFile, -7) == '.tar.gz')
{
$this->LogInfo('tar.gz file detected');
$oArchive = new TarGzArchive($sFile);
}
else
{
throw new BackupException('Unsupported format for a backup file: '.$sFile);
}
try {
IssueLog::Info('Backup Restore - LOCK acquired, executing...');
$bReadonlyBefore = SetupUtils::EnterReadOnlyMode(MetaModel::GetConfig());
// Load the database
//
$sDataDir = APPROOT.'data/tmp-backup-'.rand(10000, getrandmax());
try {
//safe zone for db backup => cron is stopped/ itop in readonly
$this->LogInfo("Starting restore of ".basename($sFile));
SetupUtils::builddir($sDataDir); // Here is the directory
$oArchive->extractTo($sDataDir);
$sDataFile = $sDataDir.'/itop-dump.sql';
$this->LoadDatabase($sDataFile);
$sNormalizedFile = strtolower(basename($sFile));
if (substr($sNormalizedFile, -4) == '.zip') {
$this->LogInfo('zip file detected');
$oArchive = new ZipArchiveEx();
$oArchive->open($sFile);
} elseif (substr($sNormalizedFile, -7) == '.tar.gz') {
$this->LogInfo('tar.gz file detected');
$oArchive = new TarGzArchive($sFile);
} else {
throw new BackupException('Unsupported format for a backup file: '.$sFile);
}
// Update the code
//
$sDeltaFile = APPROOT.'data/'.$sEnvironment.'.delta.xml';
// Load the database
//
$sDataDir = APPROOT.'data/tmp-backup-'.rand(10000, getrandmax());
if (is_file($sDataDir.'/delta.xml'))
{
// Extract and rename delta.xml => <env>.delta.xml;
rename($sDataDir.'/delta.xml', $sDeltaFile);
}
else
{
@unlink($sDeltaFile);
}
if (is_dir(APPROOT.'data/production-modules/'))
{
try
{
SetupUtils::rrmdir(APPROOT.'data/production-modules/');
}
catch (Exception $e)
{
throw new BackupException("Can't remove production-modules dir", 0, $e);
SetupUtils::builddir($sDataDir); // Here is the directory
$oArchive->extractTo($sDataDir);
$sDataFile = $sDataDir.'/itop-dump.sql';
$this->LoadDatabase($sDataFile);
// Update the code
//
$sDeltaFile = APPROOT.'data/'.$sEnvironment.'.delta.xml';
if (is_file($sDataDir.'/delta.xml')) {
// Extract and rename delta.xml => <env>.delta.xml;
rename($sDataDir.'/delta.xml', $sDeltaFile);
} else {
@unlink($sDeltaFile);
}
if (is_dir(APPROOT.'data/production-modules/')) {
try {
SetupUtils::rrmdir(APPROOT.'data/production-modules/');
} catch (Exception $e) {
throw new BackupException("Can't remove production-modules dir", 0, $e);
}
}
if (is_dir($sDataDir.'/production-modules')) {
rename($sDataDir.'/production-modules', APPROOT.'data/production-modules/');
}
$sConfigFile = APPROOT.'conf/'.$sEnvironment.'/config-itop.php';
@chmod($sConfigFile, 0770); // Allow overwriting the file
rename($sDataDir.'/config-itop.php', $sConfigFile);
@chmod($sConfigFile, 0440); // Read-only
try {
SetupUtils::rrmdir($sDataDir);
} catch (Exception $e) {
throw new BackupException("Can't remove data dir", 0, $e);
}
$oEnvironment = new RunTimeEnvironment($sEnvironment);
$oEnvironment->CompileFrom($sEnvironment);
} finally {
if (! $bReadonlyBefore) {
SetupUtils::ExitReadOnlyMode();
} else {
//we are in the scope of main process that needs to handle/keep readonly mode.
$this->LogInfo("Keep readonly mode after restore");
}
}
}
if (is_dir($sDataDir.'/production-modules'))
finally
{
rename($sDataDir.'/production-modules', APPROOT.'data/production-modules/');
IssueLog::Info('Backup Restore - LOCK released.');
$oRestoreMutex->Unlock();
}
$sConfigFile = APPROOT.'conf/'.$sEnvironment.'/config-itop.php';
@chmod($sConfigFile, 0770); // Allow overwriting the file
rename($sDataDir.'/config-itop.php', $sConfigFile);
@chmod($sConfigFile, 0440); // Read-only
try
{
SetupUtils::rrmdir($sDataDir);
}
catch (Exception $e)
{
throw new BackupException("Can't remove data dir", 0, $e);
}
$oEnvironment = new RunTimeEnvironment($sEnvironment);
$oEnvironment->CompileFrom($sEnvironment);
}
}

View File

@@ -49,7 +49,8 @@ Dict::Add('EN US', 'English', 'English', array(
'bkp-status-backups-auto' => 'Scheduled backups',
'bkp-status-backups-manual' => 'Manual backups',
'bkp-status-backups-none' => 'No backup yet',
'bkp-next-backup' => 'The next backup will occur on <b>%1$s</b> (%2$s) at %3$s',
'bkp-next-backup' => 'The next backup will occur on <b>%1$s</b> (%2$s) at %3$s.',
'bkp-next-backup-unknown' => 'The next backup is <b>not scheduled</b> yet.',
'bkp-button-backup-now' => 'Backup now!',
'bkp-button-restore-now' => 'Restore!',
'bkp-confirm-backup' => 'Please confirm that you do request the backup to occur right now.',

View File

@@ -0,0 +1,203 @@
<?php
/**
* Copyright (C) 2013-2021 Combodo SARL
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
*/
if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__));
if (!defined('APPROOT'))
{
if (file_exists(__DIR__.'/../../approot.inc.php'))
{
require_once __DIR__.'/../../approot.inc.php'; // When in env-xxxx folder
}
else
{
require_once __DIR__.'/../../../approot.inc.php'; // When in datamodels/x.x folder
}
}
require_once(APPROOT.'application/application.inc.php');
require_once(APPROOT.'application/ajaxwebpage.class.inc.php');
require_once(APPROOT.'core/log.class.inc.php');
require_once(APPROOT.'application/startup.inc.php');
require_once(dirname(__FILE__).'/dbrestore.class.inc.php');
class MyDBRestore extends DBRestore
{
/** @var Page used to send log */
protected $oPage;
protected function LogInfo($sMsg)
{
$this->oPage->p($sMsg);
}
protected function LogError($sMsg)
{
$this->oPage->p('Error: '.$sMsg);
ToolsLog::Error($sMsg);
}
public function __construct($oPage)
{
$this->oPage = $oPage;
parent::__construct();
}
}
/**
* Checks if a parameter (possibly empty) was specified when calling this page
*/
function CheckParam($sParamName)
{
global $argv;
if (isset($_REQUEST[$sParamName])) return true; // HTTP parameter either GET or POST
if (!is_array($argv)) return false;
foreach($argv as $sArg)
{
if ($sArg == '--'.$sParamName) return true; // Empty command line parameter, long unix style
if ($sArg == $sParamName) return true; // Empty command line parameter, Windows style
if ($sArg == '-'.$sParamName) return true; // Empty command line parameter, short unix style
if (preg_match('/^--'.$sParamName.'=(.*)$/', $sArg, $aMatches)) return true; // Command parameter with a value
}
return false;
}
function Usage($oP)
{
$oP->p('Restore an iTop from a backup file');
$oP->p('Parameters:');
if (utils::IsModeCLI())
{
$oP->p('auth_user: login, must be administrator');
$oP->p('auth_pwd: ...');
}
$oP->p('backup_file [optional]: name of the file to store the backup into. Follows the PHP strftime format spec. The following placeholders are available: __HOST__, __DB__, __SUBNAME__');
$oP->p('mysql_bindir [optional]: specify the path for mysql executable');
if (utils::IsModeCLI())
{
$oP->p('Example: php -q restore.php --auth_user=admin --auth_pwd=myPassw0rd --backup_file=/tmp/backup.zip');
$oP->p('Known limitation: the current directory must be the directory of backup.php');
}
else
{
$oP->p('Example: .../restore.php?backup_file=/tmp/backup.zip');
}
}
function ExitError($oP, $sMessage)
{
ToolsLog::Error($sMessage);
$oP->p($sMessage);
$oP->output();
exit;
}
function ReadMandatoryParam($oP, $sParam)
{
$sValue = utils::ReadParam($sParam, null, true /* Allow CLI */, 'raw_data');
if (is_null($sValue))
{
ExitError($oP, "ERROR: Missing argument '$sParam'");
}
return trim($sValue);
}
/////////////////////////////////
// Main program
set_time_limit(0);
if (utils::IsModeCLI())
{
$oP = new CLIPage("iTop - iTop Restore");
SetupUtils::CheckPhpAndExtensionsForCli($oP);
}
else
{
$oP = new WebPage("iTop - iTop Restore");
}
try
{
utils::UseParamFile();
}
catch(Exception $e)
{
ExitError($oP, $e->GetMessage());
}
if (utils::IsModeCLI())
{
$oP->p(date('Y-m-d H:i:s')." - running backup utility");
$sAuthUser = ReadMandatoryParam($oP, 'auth_user');
$sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd');
$sBackupFile = ReadMandatoryParam($oP, 'backup_file');
$bDownloadBackup = false;
if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd))
{
UserRights::Login($sAuthUser); // Login & set the user's language
}
else
{
ExitError($oP, "Access restricted or wrong credentials ('$sAuthUser')");
}
if (!is_file($sBackupFile) && is_readable($sBackupFile)){
ExitError($oP, "Cannot access backup file ('$sBackupFile')");
}
}
else
{
require_once(APPROOT.'application/loginwebpage.class.inc.php');
LoginWebPage::DoLogin(); // Check user rights and prompt if needed
}
if (!UserRights::IsAdministrator())
{
ExitError($oP, "Access restricted to administors");
}
if (CheckParam('?') || CheckParam('h') || CheckParam('help'))
{
Usage($oP);
$oP->output();
exit;
}
// Interpret strftime specifications (like %Y) and database placeholders
$oRestore = new MyDBRestore($oP);
$oRestore->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', ''));
$res = false;
if (MetaModel::GetConfig()->Get('demo_mode'))
{
$oP->p("Sorry, iTop is in demonstration mode: the feature is disabled");
}
else
{
$sEnvironment = utils::ReadParam('environment', 'production', false, 'raw_data');
$oRestore->RestoreFromCompressedBackup($sBackupFile, $sEnvironment);
}
$oP->output();

View File

@@ -337,18 +337,29 @@ try {
);
}
// Do backup now
// Next occurrence
//
$oBackupExec = new BackupExec();
$oNext = $oBackupExec->GetNextOccurrence();
$sNextOccurrence = Dict::Format('bkp-next-backup', $aWeekDayToString[$oNext->Format('N')], $oNext->Format('Y-m-d'),
$oNext->Format('H:i'));
/** @var \BackgroundTask $oTask */
$oTask = MetaModel::GetObjectByName(BackgroundTask::class, BackupExec::class, false);
if ($oTask)
{
$oTimezone = new DateTimeZone(MetaModel::GetConfig()->Get('timezone'));
$oNext = new DateTime($oTask->Get('next_run_date'), $oTimezone);
$sNextOccurrence = Dict::Format('bkp-next-backup', $aWeekDayToString[$oNext->Format('N')], $oNext->Format('Y-m-d'),
$oNext->Format('H:i'));
}
else
{
$sNextOccurrence = Dict::S('bkp-next-backup-unknown');
}
$oFieldsetBackupNow->AddSubBlock(
AlertUIBlockFactory::MakeForInformation('', $sNextOccurrence)
->SetIsClosable(false)
->SetIsCollapsible(false)
);
// Do backup now
//
$oLaunchBackupButton = ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('bkp-button-backup-now'));
$oLaunchBackupButton->SetOnClickJsCode('LaunchBackupNow();');
$oFieldsetBackupNow->AddSubBlock($oLaunchBackupButton);

View File

@@ -23,6 +23,7 @@ class ConfigNodesVisitor extends NodeVisitorAbstract
Node\Name::class,
Node\Const_::class,
Node\Identifier::class,
Node\Expr\Array_::class,
Node\Expr\ArrayDimFetch::class,
@@ -44,6 +45,7 @@ class ConfigNodesVisitor extends NodeVisitorAbstract
Node\Expr\PreDec::class,
Node\Expr\PreInc::class,
Node\Expr\Print_::class,
Node\Stmt\Expression::class,
Node\Expr\Ternary::class,
Node\Expr\UnaryMinus::class,
Node\Expr\UnaryPlus::class,

View File

@@ -115,3 +115,5 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
'Class:ModuleInstallation/Attribute:version' => 'Version',
'Class:ModuleInstallation/Attribute:comment' => 'Kommentar',
));

View File

@@ -83,3 +83,4 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'iTopHub:InstallationStatus:Version_NotInstalled' => 'Version %1$s <b>未</b> 安装.',
));

View File

@@ -67,6 +67,8 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', array(
Dict::Add('CS CZ', 'Czech', 'Čeština', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry~~',
'Portal:Form:Close:Warning' => 'Do you want to leave this form ? Data entered may be lost~~',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -67,6 +67,8 @@ Dict::Add('DA DA', 'Danish', 'Dansk', array(
Dict::Add('DA DA', 'Danish', 'Dansk', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry~~',
'Portal:Form:Close:Warning' => 'Do you want to leave this form ? Data entered may be lost~~',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -67,6 +67,8 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
Dict::Add('DE DE', 'German', 'Deutsch', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Dieses Objekt schließen',
'Portal:Form:Close:Warning' => 'Soll diese Eingabemaske verlassen werden? Eingegebene Daten werden nicht gespeichert.',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -67,6 +67,8 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellaño', array(
Dict::Add('ES CR', 'Spanish', 'Español, Castellaño', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry~~',
'Portal:Form:Close:Warning' => 'Do you want to leave this form ? Data entered may be lost~~',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -67,6 +67,8 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry~~',
'Portal:Form:Close:Warning' => 'Do you want to leave this form ? Data entered may be lost~~',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -67,6 +67,8 @@ Dict::Add('IT IT', 'Italian', 'Italiano', array(
Dict::Add('IT IT', 'Italian', 'Italiano', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry~~',
'Portal:Form:Close:Warning' => 'Do you want to leave this form ? Data entered may be lost~~',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -67,6 +67,8 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
Dict::Add('JA JP', 'Japanese', '日本語', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry~~',
'Portal:Form:Close:Warning' => 'Do you want to leave this form ? Data entered may be lost~~',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -70,6 +70,8 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry~~',
'Portal:Form:Close:Warning' => 'Ben je zeker dat je dit venster wil sluiten? Ingevoerde gegevens kunnen verloren gaan.',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -67,6 +67,8 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry~~',
'Portal:Form:Close:Warning' => 'Você deseja abandonar esta página? Os dados digitados podem ser perdidos.',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -76,6 +76,8 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
Dict::Add('RU RU', 'Russian', 'Русский', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry~~',
'Portal:Form:Close:Warning' => 'Вы действительно хотите закрыть эту форму? Введённые данные могут быть утеряны.',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -67,6 +67,8 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', array(
Dict::Add('SK SK', 'Slovak', 'Slovenčina', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry~~',
'Portal:Form:Close:Warning' => 'Do you want to leave this form ? Data entered may be lost~~',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -67,6 +67,8 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry~~',
'Portal:Form:Close:Warning' => 'Do you want to leave this form ? Data entered may be lost~~',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -67,6 +67,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry~~',
'Portal:Form:Close:Warning' => 'Do you want to leave this form ? Data entered may be lost~~',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.~~',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.~~',
));
// UserProfile brick

View File

@@ -1630,6 +1630,21 @@ table .group-actions {
.cke_toolbox_collapser, .cke_toolbox_collapser .cke_arrow {
cursor: pointer !important;
}
.cke_toolbox_collapser.cke_toolbox_collapser_min ~ .editor-fullscreen-button {
display: block;
width: 12px;
height: 11px;
border: 1px solid #ddd;
cursor: pointer;
/* !important so it overrides the .cke_reset_all style */
background-position: center center !important;
background-repeat: no-repeat !important;
background-size: 100% !important;
background-image: url('../../../../../images/full-screen.png') !important;
}
.cke_toolbox_collapser.cke_toolbox_collapser_min ~ .editor-fullscreen-button:hover {
background-color: #E5E5E5;
}
/* DataTables : Selection inputs */
.dataTable.table th span.row_input, .dataTable.table td span.row_input {
display: inline-block;

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