Compare commits

...

574 Commits
2.0 ... 2.0.3

Author SHA1 Message Date
Romain Quetiez
f844cef03e Releasing 2.0.3
SVN:2.0.3[3275]
2014-07-15 13:36:17 +00:00
Denis Flaven
4c0e176f3e #960 [RPM Packaging] Adjust line endings in READ and LICENSE files
#962 [RPM Packaging] Added the use of logrotate for cron.log and error.log

SVN:trunk[3274]
2014-07-15 09:21:47 +00:00
Denis Flaven
f0f519e351 #959 Fixing licensing mismatches for compatibility with the Fedora licensing policy (the modification only affects comments) .
SVN:trunk[3273]
2014-07-15 08:16:31 +00:00
Denis Flaven
9a68890660 Integration of the additions to the Russian localization by Vladimir Kunin.
SVN:trunk[3272]
2014-07-15 08:02:49 +00:00
Denis Flaven
806ed2102c [RPM Packaging] Preparing for the 2.0.3 release.
- Added php-ldap to the list of dependencies

SVN:trunk[3271]
2014-07-14 18:54:04 +00:00
Denis Flaven
14b9778c50 [RPM Packaging] small fix in the install script to adjust the rights in /etc/httpd/conf.d
SVN:trunk[3270]
2014-07-14 18:49:27 +00:00
Denis Flaven
0142910c26 #958 [RPM Packaging] Integrated Igor's patch to support both Apache 2.2 and 2.4.
SVN:trunk[3269]
2014-07-14 17:26:04 +00:00
Romain Quetiez
0068d7c8be Rolling back on the exclusion of portal users from the REST/JSON API. Let's communicate on this future change and leave some time for integrators to get ready for the change.
SVN:trunk[3268]
2014-07-11 14:37:55 +00:00
Romain Quetiez
302811986e Update for the upcoming release 2.0.3
SVN:trunk[3267]
2014-07-11 14:23:11 +00:00
Romain Quetiez
f773433a93 New REST service: core/check_credentials to implement single sign-on solutions
SVN:trunk[3266]
2014-07-11 14:21:30 +00:00
Denis Flaven
880653f191 Fixed a regression introduced by [r3242] (warning when sending an email with an empty "To")
SVN:trunk[3265]
2014-07-11 13:27:13 +00:00
Romain Quetiez
02ee41ef09 Readme ready for the Release Candidate of 2.0.3
SVN:trunk[3264]
2014-07-10 13:00:42 +00:00
Denis Flaven
d6344568c7 Update of the SwiftMailer licence
SVN:trunk[3263]
2014-07-10 12:51:09 +00:00
Denis Flaven
38a2fd75c0 Prevent a warning when sending an email with no recipient in To:
SVN:trunk[3262]
2014-07-10 12:49:39 +00:00
Denis Flaven
7ca8c3e834 One small step for making the portal more generic...
SVN:trunk[3261]
2014-07-10 12:48:00 +00:00
Romain Quetiez
7d3a93c374 #955 Translated the label "Open Requests" in the welcome menu
SVN:trunk[3260]
2014-07-10 10:29:44 +00:00
Denis Flaven
a978bb9c17 CSV import: "Ignore this field" can be used more than one time in the mapping.
SVN:trunk[3259]
2014-07-10 08:44:52 +00:00
Denis Flaven
3ed01cde51 Fixed regression introduced by "Provide feedback while loading asynchronous tabs.", provide feeback ONLY when loading!
SVN:trunk[3258]
2014-07-10 08:34:01 +00:00
Denis Flaven
67471e2636 Fix for a warning about mysql_free() revealing a problem with DBObjectSet
SVN:trunk[3257]
2014-07-10 08:27:16 +00:00
Denis Flaven
4c44159a88 Global search finalization:
- Skip abstract classes, since we search in all the child classes anyway
- Search in a class explicitely requested even it should be skipped by config
- Remove the "no object found" message if the "enlarged search" finds something

SVN:trunk[3256]
2014-07-09 10:32:30 +00:00
Romain Quetiez
5f5125a131 #854...ROLLBACK, undoing the commit [3185]. The implementation has been postponed to the next major relase.
SVN:trunk[3255]
2014-07-09 09:49:02 +00:00
Denis Flaven
f6d0bda737 Properly render the "CONCAT_WS" SQL expression used by the global search.
SVN:trunk[3254]
2014-07-09 09:47:21 +00:00
Denis Flaven
a64b299644 "Portal Users" are not allowed to use the REST/JSON webservices. This case is now properly handled with a specific message.
SVN:trunk[3253]
2014-07-08 10:47:50 +00:00
Denis Flaven
e022bf03db Oops: Protect against non-existing parameters... (occurs when configuring a list)
SVN:trunk[3252]
2014-07-08 10:01:47 +00:00
Denis Flaven
bab6ca7954 Protection against a non existing UserLDAP class (in case the LDAP module is not installed)
SVN:trunk[3251]
2014-07-07 17:02:40 +00:00
Romain Quetiez
bb70884041 Readme file ready for 2.0.3 (RC), planned for the 15th
SVN:trunk[3250]
2014-07-07 15:55:27 +00:00
Denis Flaven
58cb67ecd3 Protect against non-existing parameters... (occurs when configuring a list)
SVN:trunk[3249]
2014-07-07 10:25:52 +00:00
Denis Flaven
4920cc4aee Fix in the log of the setup: Properly compute the percentage of "useless" records in priv_change.
SVN:trunk[3248]
2014-07-04 15:30:36 +00:00
Denis Flaven
12c587896e #927 Detect IE7 (or compatibility mode) and warn the user that the application cannot be used with such browsers.
SVN:trunk[3247]
2014-07-04 15:22:00 +00:00
Denis Flaven
eb74288c85 Added an index on "item_org_id" to speed-up the setup and the silo on organizations.
SVN:trunk[3246]
2014-07-04 14:15:24 +00:00
Denis Flaven
922049e8f2 Provide feedback while loading asynchronous tabs.
SVN:trunk[3245]
2014-07-04 13:18:47 +00:00
Denis Flaven
d83edb803b Better handling of the user's configured display limit for displaying the CSV import history. Also provide feedback while reloading.
SVN:trunk[3244]
2014-07-04 13:18:07 +00:00
Denis Flaven
9f41c7e577 Automatic cleanup of the table 'priv_change' before upgrading.
SVN:trunk[3243]
2014-07-04 13:09:27 +00:00
Denis Flaven
1335a15c30 Fixed a regression introduced by [r3141]: display of objects templates was broken.
SVN:trunk[3242]
2014-07-03 16:51:50 +00:00
Romain Quetiez
d847272264 Reworked the asynchronous mechanism to ease its extension (added a "status", preset by the setup)
SVN:trunk[3241]
2014-07-03 14:31:54 +00:00
Denis Flaven
8054d7b17a #954 Stricter validation of mandatory tags/values during compilation.
SVN:trunk[3240]
2014-07-03 12:26:04 +00:00
Romain Quetiez
1ba2ed809c Added a warning when upgrading the application while a CRON is being executed on the same DB
SVN:trunk[3239]
2014-07-03 07:11:43 +00:00
Denis Flaven
b2feca5eb5 #944 Better initialization of the 'origin' field based on localized comments.
SVN:trunk[3238]
2014-07-02 09:32:05 +00:00
Denis Flaven
87ce86880e Added Jean-François Bilger to the "Thank you" section
SVN:trunk[3237]
2014-07-01 17:03:05 +00:00
Denis Flaven
d4de0d2ee3 Fixed a regression introduced by [3188], which affects the Audit results and also potentially the User rights. Many thanks to Jean-Francois Bilger for finding the issue (and the associated fix) !
SVN:trunk[3236]
2014-07-01 16:40:38 +00:00
Denis Flaven
730f522ab2 #944 Automatically create an index on the 'origin' field.
SVN:trunk[3235]
2014-07-01 15:44:24 +00:00
Denis Flaven
008cac25b4 #944 Speed-up the display of CSV import history. IF the feature is enabled, the CSV import history is:
1) Displayed asynchronously
2) Based on a specific 'origin' enum fields instead of parsing the userinfo comment

SVN:trunk[3234]
2014-07-01 14:09:18 +00:00
Denis Flaven
ede552968b Added the (missing) field status to the details of VirtualDevice, Hypervisor and Farm.
Fixed two French translations.

SVN:trunk[3233]
2014-07-01 09:15:54 +00:00
Denis Flaven
686848a0ae #953 Protect the background processing against corrupted (incomplete) objects.
SVN:trunk[3231]
2014-06-30 15:24:49 +00:00
Denis Flaven
61b5b5cc71 #906 Better way to handle the lock in order to prevent duplicates in the numbering of Tickets. Note that the iTopMutex now supports re-entrancy inside the same PHP page.
SVN:trunk[3230]
2014-06-27 13:45:48 +00:00
Denis Flaven
ec16c5f86f #940 "Close" and "Reopen" buttons were not working on IE (due to extra commas at the end of JS lists)
SVN:trunk[3228]
2014-06-27 11:02:52 +00:00
Denis Flaven
d602bb2190 #948 Finalization of the full text accelerators: two new parameters: 'skip' and 'enable_enlarge'. 'attributes' is now an optional parameter.
SVN:trunk[3227]
2014-06-26 14:27:26 +00:00
Denis Flaven
1b9d3d3304 Applied again the patch [r3025] to support an empty TO: header when using the PHP Mail transport.
SVN:trunk[3226]
2014-06-26 12:16:26 +00:00
Denis Flaven
02e5d57998 #949 Upgraded SwiftMailer to the latest version (5.2.1)
SVN:trunk[3225]
2014-06-26 12:07:35 +00:00
Denis Flaven
6236d96a65 #951 Properly display localized values for ENUMs in the data model view.
SVN:trunk[3224]
2014-06-25 16:51:26 +00:00
Denis Flaven
6473a8f245 #892 Refresh the "multi-select" when picking a value with the "hierarchical picker", inside a search form.
SVN:trunk[3223]
2014-06-25 14:37:13 +00:00
Denis Flaven
ab93eaefff #916 Hierarchical selector: clicking on the label now has the same effect as clicking on the radio button itself. (The label is no longer an hyperlink)
SVN:trunk[3222]
2014-06-24 15:56:24 +00:00
Denis Flaven
037803c033 Protect attachments against concurrent edition of a ticket.
SVN:trunk[3221]
2014-06-24 10:03:34 +00:00
Denis Flaven
06582cfe35 Oops, fixed a typo in: Add the ability to supply a default "from" email address for the "forgot password" feature, instead of using the same address as for the "to".
SVN:trunk[3219]
2014-06-18 15:57:25 +00:00
Denis Flaven
a72e15023f Proper handling of the re-entrence of the Lock/Unlock within the same PHP page. Previously the code was sometimes performing 2 Unlock() for each Lock().
SVN:trunk[3217]
2014-06-18 15:24:31 +00:00
Romain Quetiez
7ddd74a95e Fix to complete the commit [3193]: template abusively selected on the user portal
SVN:trunk[3216]
2014-06-17 13:10:41 +00:00
Denis Flaven
4fccf5c815 Add the ability to supply a default "from" email address for the "forgot password" feature, instead of using the same address as for the "to".
SVN:trunk[3213]
2014-06-16 15:14:40 +00:00
Romain Quetiez
a6d984e23f Removed a debug trace introduced with the file drag&drop capability
SVN:trunk[3212]
2014-06-13 09:48:31 +00:00
Denis Flaven
7bf3f97a72 Release of the 2.0.3-beta, adjusting the date!
SVN:trunk[3211]
2014-06-13 09:29:23 +00:00
Denis Flaven
e78743d309 Fixed a regression introduced by #930: a field can depend on a hidden fields, but this has no visible effect.
SVN:trunk[3210]
2014-06-13 09:10:17 +00:00
Denis Flaven
6b86ac5090 Protect the optimization in case the object's friendlyname is requested.
SVN:trunk[3209]
2014-06-12 14:45:33 +00:00
Romain Quetiez
14c78cb543 Fixed a regression on the global search, introduced in the commit [3188]
SVN:trunk[3208]
2014-06-12 09:27:09 +00:00
Denis Flaven
5007e6ffc3 Updating the readme and the licence with the drag and drop feature.
SVN:trunk[3207]
2014-06-11 07:43:15 +00:00
Denis Flaven
1d4784b0c7 Allow uploading files as Tickets' attachments via drag and drop (on "modern" browsers supporting HTML5 drag and drop)
SVN:trunk[3206]
2014-06-10 15:05:02 +00:00
Denis Flaven
d497020733 Oups, version number back to: 1.0.5 = beta 6, with the correct collection of VLANs as DistributedVirtualPortgroups
SVN:trunk[3205]
2014-06-10 15:01:45 +00:00
Romain Quetiez
98afb2dd49 Datamodel version changed to 2.0.3
SVN:trunk[3204]
2014-06-10 14:14:27 +00:00
Romain Quetiez
f89792e171 Demo mode: the profiles must be in read-only mode (it is not enough to have the users in read-only)
SVN:trunk[3203]
2014-06-10 11:50:20 +00:00
Romain Quetiez
8141f8c619 Fixed the readme file for 2.0.3 Beta
SVN:trunk[3202]
2014-06-06 15:53:14 +00:00
Denis Flaven
2009a10204 Preparing the readme for iTop 2.0.3-beta.
SVN:trunk[3201]
2014-06-06 14:18:48 +00:00
Romain Quetiez
d2961c585e #926 JSON/REST Delete: nasty report
SVN:trunk[3200]
2014-06-06 09:50:57 +00:00
Denis Flaven
9b5fc043cb #943: Fix for supporting drop-down lists/auto-completes based on a parametrized query in the portal.
SVN:trunk[3199]
2014-06-06 09:43:20 +00:00
Romain Quetiez
67ef671632 #865 CSV import : mapping several columns to the same fields gives an error. Fix: the wizard does not allow the user to move forward.
SVN:trunk[3198]
2014-06-05 14:42:53 +00:00
Romain Quetiez
d07ca49e53 #636 and #861 Set the focus on User Name in iTop Login Form
SVN:trunk[3197]
2014-06-05 09:48:59 +00:00
Denis Flaven
44aff84dfa #934: Support the <display_style> option for ExternalKeys.
SVN:trunk[3196]
2014-06-05 09:19:15 +00:00
Romain Quetiez
c9ffdb9342 Cleanup: the model.x file will be rebuilt from the xml file anyway!!!
SVN:trunk[3195]
2014-06-04 14:51:36 +00:00
Romain Quetiez
65343a485f Completed the portuguese translation (Brazil), provided in december... provided some time ago (by whom?) - modules updated: attachments, change, incident, request and request/ITIL, service for providers
SVN:trunk[3194]
2014-06-04 14:29:22 +00:00
Romain Quetiez
74cda2831f Portal + templates: Bug fix = when the user selects a template, then go back to select a service for which no template applies, he still gets the tempate fields in the final form.
SVN:trunk[3193]
2014-06-04 13:37:58 +00:00
Romain Quetiez
c4632cda1a Added a helper function to get an icon stored as an ormDocument: ormDocument::GetDownloadURL
SVN:trunk[3191]
2014-06-04 13:10:23 +00:00
Romain Quetiez
d7ba4166e5 #919 Circular references between tickets (parent/child). Protect the framework against infinite recursions on cascaded updates (done at the DBUpdate level).
SVN:trunk[3190]
2014-06-03 18:05:31 +00:00
Romain Quetiez
ee3795ef6d Full text search shortcuts: allow the use of class names containing underscores and numbers (e.g. Processus métier: écarissage)
SVN:trunk[3189]
2014-06-03 17:57:53 +00:00
Romain Quetiez
e8a7695353 #731 Full text search requires a string of at least three characters (configurable: full_text_needle_min)
SVN:trunk[3188]
2014-06-03 14:42:17 +00:00
Romain Quetiez
e4bb0acd94 #936 Tune the default (i.e. implicit) tracking level on link sets (and disable tracking on 1-N links, for fresh installations)
SVN:trunk[3187]
2014-06-03 13:36:35 +00:00
Denis Flaven
1572d9da5a #930 AttributeExternalFields displayed in a form are automatically refreshed when their "parent" field is modified...
SVN:trunk[3186]
2014-06-02 16:05:20 +00:00
Romain Quetiez
2d0ca37f27 #854 Flag Is null allowed not working on attributes Date and DateTime + the default value is now operant
SVN:trunk[3185]
2014-06-02 14:35:28 +00:00
Romain Quetiez
df09182878 Oooops... completes the commit 3175 (#929 Speed up the full text search)
SVN:trunk[3184]
2014-06-02 14:22:14 +00:00
Denis Flaven
20bb9a62bb #935 Better support of CheckToWrite() in object's transitions, improved by checking the data sooner for a consistent workflow.
SVN:trunk[3183]
2014-06-02 13:16:37 +00:00
Romain Quetiez
a6f5436d07 #873 Allow the character % in the path of an URL (requires the edition of the config file when upgrading)
SVN:trunk[3182]
2014-06-02 10:23:06 +00:00
Denis Flaven
53e58d5887 #872 Support notifications for the creation of a new user. Also fix the translation of the "Additional values" in ValueSetEnumClasses.
SVN:trunk[3181]
2014-05-30 17:11:37 +00:00
Denis Flaven
ef1178c78d #935 Better support of CheckToWrite() in object's transitions
SVN:trunk[3180]
2014-05-30 16:43:14 +00:00
Denis Flaven
5456d9d20a #933 Search form for Query Phrasebook items. If you implement your own menus (equivalent to itop-welcome-itil module), make sure that you update the menu definition to show the search form at the top.
SVN:trunk[3179]
2014-05-30 07:42:17 +00:00
Denis Flaven
7c8ff071d2 #931 Management of n:n links can be broken in case of insufficient user rights. Side effect: attribute_linkedset with the flag OPT_ATT_HIDDEN are now completely hidden (the tab is not displayed at all).
SVN:trunk[3178]
2014-05-28 16:37:25 +00:00
Romain Quetiez
e4010b4b13 #886 Delete change history so that if an ID is reused the history starts from scratch (and cleanup most of the data as soon as the object is deleted)
SVN:trunk[3177]
2014-05-28 16:00:24 +00:00
Denis Flaven
dbb9558b45 #862: Popup menu misplaced when the window scrolls (e.g. when displaying large lists of results)
SVN:trunk[3176]
2014-05-28 15:22:57 +00:00
Romain Quetiez
8f83970239 #929 Speed up the full text search (mostly from the end user perspective, requires a custom configuration)
SVN:trunk[3175]
2014-05-26 15:42:47 +00:00
Romain Quetiez
102b6c248b #928 Setup crashing if async_retries is configured
SVN:trunk[3174]
2014-05-26 14:45:07 +00:00
Denis Flaven
946b4212d3 Fix of a regression introduced by [3146]: due to formatting rules, empty friendlynames may not look like empty strings!
SVN:trunk[3172]
2014-05-23 15:49:30 +00:00
Denis Flaven
e2e6861b03 Properly handle external and basic authentication methods for REST web services.
SVN:trunk[3170]
2014-05-23 13:53:20 +00:00
Denis Flaven
7b7e69a890 Properly optimize the columns to load, when subitems are requested.
SVN:trunk[3168]
2014-05-23 13:08:31 +00:00
Romain Quetiez
92b2131d3b #483 Added placeholders for the notifications: html(caselog), head_html(caselog), html(linkset). The HTML can be customized. Fixes the issue about lines being wrapped in a curious way (root cause: swift mailer).
SVN:trunk[3167]
2014-05-23 12:32:08 +00:00
Romain Quetiez
43130aef71 Allow the use of any character into the help text on an attribute (usefull to explain a constraint implemented as a regular expression for instance.) Reminder: the text is given as a dictionary entry named like "Class:<class>/Attribute:<attcode>?"
SVN:trunk[3166]
2014-05-23 08:45:36 +00:00
Romain Quetiez
ba4e5ec786 #925 Completes the commit [3163], the link sets were still not correctly handled
SVN:trunk[3164]
2014-05-22 13:23:47 +00:00
Romain Quetiez
3e79dad435 #925 REST/JSON: Added an option to output all the fields of the object found (not only the fields of the queried class)
SVN:trunk[3163]
2014-05-20 08:15:53 +00:00
Romain Quetiez
b0a84f96f1 #897 Rest Services: improved the error reporting when an external key is specified with a final class that is not a subclass of the class of the external key
SVN:trunk[3162]
2014-05-19 15:02:42 +00:00
Romain Quetiez
85fa578f2a #876 Upgrade finishes with error "Cannot reload object id = -1" (root cause: DB in read-only mode, see config/access_mode)
SVN:trunk[3161]
2014-05-19 14:23:52 +00:00
Romain Quetiez
364bce90e4 #859 About box: also list the modules installed from the extensions folder
SVN:trunk[3160]
2014-05-19 11:43:45 +00:00
Romain Quetiez
fcfc1e7307 #875 Could not use OQL queries with a double quote in the condition
SVN:trunk[3159]
2014-05-19 08:28:39 +00:00
Romain Quetiez
85cb04a3f3 Fixed a compiler error message (wrong syntax when using a PHP class to implement the class methods)
SVN:trunk[3158]
2014-05-19 07:28:26 +00:00
Denis Flaven
ec77e58276 Limit the display of the status to the latest 100 runs of the synchro data source.
SVN:trunk[3157]
2014-05-16 05:02:38 +00:00
Denis Flaven
196fba7d81 Implement the iDisplay interface on any class derived from DBObject, but also limit the possible actions on such objects (disable edition)
SVN:trunk[3156]
2014-05-16 05:01:45 +00:00
Denis Flaven
317344da2e Proper output of boolean values in JSON.
SVN:trunk[3155]
2014-05-16 04:58:45 +00:00
Romain Quetiez
cd37a78800 Fixed a regression due to the optimization of the memory usage of DBObjectSet [3142]
SVN:trunk[3154]
2014-05-15 13:54:45 +00:00
Denis Flaven
9ed5ceb11e Fix for a regression introduced by [3149]
SVN:trunk[3153]
2014-05-14 13:47:40 +00:00
Denis Flaven
f47327fdd4 Second fix for a regression due to revisions 3141/3150
SVN:trunk[3152]
2014-05-14 13:24:19 +00:00
Denis Flaven
5ec37c8060 Fix for a regression due to revisions 3141/3150
SVN:trunk[3151]
2014-05-14 13:16:41 +00:00
Denis Flaven
bb65153351 Fixed regression introduced by [3141]: Code cleanup to implement the tabs handling (inside web pages) in one place.
SVN:trunk[3150]
2014-05-07 13:14:19 +00:00
Denis Flaven
389b97dc50 Two experimental perf. enhancements:
- maintain list the attributes (potentially) modified to speed-up ListChanges() by avoiding a systematic comparison between the content of linkedsets.
- cache the list of SynchroDataSources and use this in InSyncScope() to avoid searching in the SynchroReplicas when it's not needed...

Depending on the configuration, these optimizations may speed-up the CSV import by up to 40% !!

SVN:trunk[3149]
2014-05-06 15:45:04 +00:00
Denis Flaven
31e30810f6 Experimental perf. enhancement: cache the foreign keys to use when importing object to avoid searching for the same object several times during a given import. Seem to speed up the imports by 7 to 10%.
SVN:trunk[3148]
2014-05-06 15:39:01 +00:00
Denis Flaven
4bffe7aec9 Oops, removing a debug trace...
SVN:trunk[3147]
2014-05-06 15:34:26 +00:00
Denis Flaven
fafa442b08 Avoid using an attribute which may cause a Reload()... seems useless anyway.
SVN:trunk[3146]
2014-05-06 15:25:00 +00:00
Denis Flaven
b095c6b1a3 #909: faster display for the "details" of an object:
- object's history is only loaded when the "History" tab is clicked
- by default the history display is truncated to the 'max_history_length' (= 50) latest modifications

SVN:trunk[3145]
2014-05-06 15:15:57 +00:00
Denis Flaven
d950422912 #867 (and #907 as a dup') De-harcode set_time_limit (per loop) in lengthy operations. Default value is 30 seconds (per loop), configurable via the new parameter "max_execution_time_per_loop", instead of 5 seconds previously.
SVN:trunk[3144]
2014-05-06 14:09:55 +00:00
Denis Flaven
b504830f45 CSV export (from the toolkit menu) now displays an asynchronous page, to better cope with a huge number of objects (> 10000)
SVN:trunk[3143]
2014-05-06 13:54:45 +00:00
Denis Flaven
4dabb566a8 - Memory optimization: no longer store all DBObjects in memory while browsng through a Set, but pull them one by one from the MySQL client buffer as needed.
- Also renamed Merge to Append since it's really what it does (seems to be used only in the tests)

SVN:trunk[3142]
2014-05-06 13:46:45 +00:00
Denis Flaven
7459ec4844 Code cleanup to implement the tabs handling (inside web pages) in one place. Added the ability to provide asynchronously loaded tabs (content must come from the same server).
SVN:trunk[3141]
2014-05-06 13:42:18 +00:00
Denis Flaven
678f982024 #923: prevent XSS injection in forgot password page.
SVN:trunk[3139]
2014-05-06 08:26:54 +00:00
Denis Flaven
d628c4f670 Use the object oriented verison of the MySQLi API which seems free of memory leaks (compared to the procedural version of the same API).
SVN:trunk[3138]
2014-05-02 14:00:42 +00:00
Denis Flaven
e1336d7ebc Run Query enhancements
- Properly catch *all* exceptions and redisplay the entered OQL statement every time
- Post the form to force its refresh (i.e. running the query again) even if the query did not change

SVN:trunk[3137]
2014-05-02 13:30:08 +00:00
Romain Quetiez
01496f9595 #918 TTO/TTR status "passed" gets reset when the stop watch is stopped (using the status "triggered" instead)
SVN:trunk[3130]
2014-04-24 08:53:17 +00:00
Denis Flaven
a60d60bfab Better handling of the default choices in case of upgrade (for some specific configurations of the installation wizard).
SVN:trunk[3127]
2014-04-15 13:01:06 +00:00
Romain Quetiez
31cb0065b5 Integrated the portal with the module to dispatch incidents or user requests (the previous commit introduced too many changes, I did rollback unnecessary stuff)
SVN:trunk[3125]
2014-04-11 14:53:36 +00:00
Romain Quetiez
4dbc5d97b8 Integrated the portal with the module to dispatch incidents or user requests
SVN:trunk[3124]
2014-04-11 14:42:57 +00:00
Romain Quetiez
4800da1653 #913 Error when searching for child requests and no organization is specified. Still, I could not figure out WHY IT WAS WORKING WHEN AN ORG IS SELECTED as a search filter!
SVN:trunk[3123]
2014-04-09 14:00:09 +00:00
Denis Flaven
8f25fb8e64 #878: Missing sscrollbar in "linkset-direct" edition popup dialog
#881: Paginated list in popup dialog is broken
- Missing scrollbar in the popup when using the [+] button

SVN:trunk[3121]
2014-04-01 13:09:02 +00:00
Denis Flaven
b43884a760 Object's edition: keep track of what was typed in the case log fields when reloading the form (for example with a different "initial state")
SVN:trunk[3119]
2014-04-01 12:06:35 +00:00
Denis Flaven
caef02720c Protect Bulk Modify against XSS injection!
SVN:trunk[3117]
2014-04-01 10:12:11 +00:00
Denis Flaven
ce9806b01c Allow re-entrance in the same named mutex within the same PHP page.
SVN:trunk[3115]
2014-04-01 09:50:21 +00:00
Romain Quetiez
7a193d1c24 Fixed issue with 1.x datamodels: dashlets of type "badge" not working (preventing from editing an existing dashboard), since 2.0.2
SVN:trunk[3113]
2014-04-01 09:14:06 +00:00
Romain Quetiez
31ea53435e Aligned the authentication module with the one of 2.x, to enable the feature "Forgot password" for legacy data models
SVN:trunk[3111]
2014-04-01 08:31:08 +00:00
Denis Flaven
dcb48d0f35 Bug fix : missing semicolons were causing an error with IE9.
SVN:trunk[3107]
2014-03-26 14:58:32 +00:00
Romain Quetiez
40aa78bb3d Finalized the French transalation for some types of "Triggers"
SVN:trunk[3106]
2014-03-24 10:21:09 +00:00
Romain Quetiez
82e9e42939 #901 Added the attribute "filter" to the triggers
SVN:trunk[3104]
2014-03-21 10:52:20 +00:00
Romain Quetiez
1ecec1dd6d #905 The toolkit menu was visible in the portal for Administrators (but it was not usable). It is now hidden in any case.
SVN:trunk[3103]
2014-03-20 10:27:13 +00:00
Denis Flaven
8dc5b05ac4 #870: when a user deletes all her/his shortcuts at once, this was deleting all the shortcuts for all users.
SVN:trunk[3099]
2014-03-12 14:05:37 +00:00
Denis Flaven
3b257eeb3a Templates processing aligned with "templates-base" 2.1.1: allow template fields with the same name the attribute code of the curent object.
SVN:trunk[3097]
2014-03-11 14:03:14 +00:00
Denis Flaven
2574a0c8a4 #887 short term fix for preventing ToArgs to alter the content of an object...
SVN:trunk[3095]
2014-03-04 13:58:28 +00:00
Romain Quetiez
404f6772fd #896 XSS injection on the portal (any search form)
SVN:trunk[3093]
2014-03-03 14:46:48 +00:00
Denis Flaven
d6dbe0fce7 Trac #890: Dispatch the defines in the proper modules to make sure that the portal works with all possible combinations of tickets.
SVN:trunk[3092]
2014-03-03 13:48:56 +00:00
Romain Quetiez
8e26ca763b Internal: modified the prototype of GetDelta to add several attributes on the root node
SVN:trunk[3090]
2014-02-26 11:24:10 +00:00
Denis Flaven
b4bc3ad716 Trac #891: better error reporting when either the parameter auth_user or auth_pwd are missing.
SVN:trunk[3089]
2014-02-25 13:37:01 +00:00
Denis Flaven
80fac28106 Trac #890: Dispatch the defines in the proper modules to make sure that the portal works with all possible combinations of tickets.
SVN:trunk[3088]
2014-02-25 13:17:25 +00:00
Romain Quetiez
5f11c97aef #888 Security on the portal incompatible with customizations (regression introduced in 2.0.2), now requires to define PORTAL_USERREQUEST_DISPLAY_QUERY and PORTAL_USERREQUEST_DISPLAY_POWERUSER_QUERY
SVN:trunk[3086]
2014-02-24 15:52:04 +00:00
Denis Flaven
d36a03bfc3 Make the Basic Authentication (login_mode=basic) work with non-ASCII characters (in the username as well as in the password), though this may depend on the browser...
SVN:trunk[3084]
2014-02-19 17:34:53 +00:00
Denis Flaven
8e0c57fce0 Add a new flag "debug" (false by default) to turn off the debug traces of the module since the traces contain potentially sensitive information in clear text.
SVN:trunk[3083]
2014-02-14 17:51:50 +00:00
Romain Quetiez
cb4c2a8e84 Internal: Manage nested subforms in property sheet edition mode
SVN:trunk[3082]
2014-02-12 14:14:01 +00:00
Denis Flaven
f993f07751 Enhanced API to support icon upload...
SVN:trunk[3081]
2014-02-10 15:41:32 +00:00
Romain Quetiez
0167a66973 Demo mode: disable the pin button on the left pane (and keeps it open and resizable)
SVN:trunk[3080]
2014-02-10 13:21:21 +00:00
Romain Quetiez
b7d8953ecb Fix for Plugins: fixed a regression introduced in 3072 (dashboards not editable at all!!!)
SVN:trunk[3079]
2014-02-06 16:27:22 +00:00
Romain Quetiez
cba75527b3 #877 REST/JSON More flexibility on case log updates (in particular, it is now possible to write the entire case log), remains compatible with the previous API
SVN:trunk[3078]
2014-02-06 11:40:26 +00:00
Denis Flaven
bc0f48721b Bug fix: the JSON value for an enum should be the raw value, not its translated label.
SVN:trunk[3077]
2014-02-06 09:22:35 +00:00
Denis Flaven
a6693d9535 Protect the initialization with a try ... catch, in order to protect that rest of the page in case of trouble.
SVN:trunk[3074]
2014-02-05 17:08:33 +00:00
Romain Quetiez
6f2c404415 Fix for Plugins: if a page uses set_base then JS popup menu items were reloading the page. Still, set_base should not be used!
SVN:trunk[3072]
2014-02-04 16:04:50 +00:00
Romain Quetiez
b00aae2536 Enabled KPI tracing for the export page... fixed regression on the setup page (rewrote the change in a much more defensive way)
SVN:trunk[3071]
2014-02-04 14:20:20 +00:00
Romain Quetiez
6334370ef0 Optimization: map the extended attribute code to the corresponding external field when this if possible (ex: org_id->name to org_name); this reduces the number of queries, in particular when using the "export CSV" menu on a list.
SVN:trunk[3069]
2014-02-03 14:35:05 +00:00
Romain Quetiez
868748efb3 Enabled KPI tracing for the export page
SVN:trunk[3067]
2014-02-03 14:26:19 +00:00
Romain Quetiez
c67e7e18e6 Optimize the queries for the export page
SVN:trunk[3066]
2014-02-03 14:23:55 +00:00
Romain Quetiez
16f1fd56ec Resetting the stop watch...do clean the first start date when it is not running!
SVN:trunk[3065]
2014-02-03 14:22:37 +00:00
Romain Quetiez
1cab84e793 Allow to reset a running stop watch (without stopping it!)
SVN:trunk[3063]
2014-02-03 11:56:22 +00:00
Denis Flaven
991fe9ccc1 Compatibility with APCu (For PHP 5.5+), since it slightly different from APC.
SVN:trunk[3061]
2014-01-22 15:18:48 +00:00
Denis Flaven
0a53f8ec3f Preserve "hidden" template fields.
SVN:trunk[3060]
2014-01-22 14:09:53 +00:00
Denis Flaven
cd7af7a2ce Dictionary string for the portal should not depend on a module
SVN:trunk[3059]
2014-01-22 14:08:34 +00:00
Denis Flaven
319b3b81ce - Put back support of templates
- Make sure that unwanted parameters cannot be set when creating the ticket

SVN:trunk[3058]
2014-01-21 10:47:45 +00:00
Denis Flaven
4235eae9b3 #871: eMail validation pattern was too strict: now fully configurable (globally and per attribute).
SVN:trunk[3056]
2014-01-17 09:39:19 +00:00
Romain Quetiez
32ec19e09d #869 REST JSON was not outputing case log attributes (implemented in a structured way)
SVN:trunk[3054]
2014-01-15 11:04:25 +00:00
Denis Flaven
63ea142168 Record the very same installation time for all modules.
SVN:trunk[3052]
2014-01-03 14:00:44 +00:00
Denis Flaven
23ec4faa65 Test commit: bumped the year to 2014.
SVN:trunk[3051]
2014-01-02 18:02:39 +00:00
Denis Flaven
da36fc673e Test commit: just bumping the copyright year
SVN:trunk[3050]
2014-01-02 17:59:39 +00:00
Romain Quetiez
3b65f33325 Internal: correctly quote XPath literals within GetNodeById
SVN:trunk[3049]
2013-12-17 14:52:20 +00:00
Romain Quetiez
e980b051b1 Asynchronous emails: added a retry mechanism useful in case your SMTP server restricts the number of emails that can be sent over a period of time (usage: broadcasting a newsletter). The mechanism is not specific to sending email as it is implemented at the AsyncTask level.
SVN:trunk[3047]
2013-12-13 10:35:37 +00:00
Denis Flaven
c84a22c503 #856: allow asynchronous emails to have an empty 'to' recipient... (not used anyway)
SVN:trunk[3043]
2013-12-12 18:02:32 +00:00
Denis Flaven
5c2578169e Add "finalclass" as a reconciliation key on all abstract classes derived from FunctionalCI
SVN:trunk[3040]
2013-12-11 10:08:23 +00:00
Romain Quetiez
645731a76d Integrated an update of the portuguese (brazil) localization, made by Marco Tulio
SVN:trunk[3039]
2013-12-11 09:38:15 +00:00
Denis Flaven
3de2d654a0 Protection against attemp to delete a non-existing node in the XML...
SVN:trunk[3038]
2013-12-10 16:43:22 +00:00
Romain Quetiez
934e500253 Setup: fixed issue when upgrading a DB (no install dir specified, thus no config file) and requesting a backup: the backup is created without the config file in it.
(Updated the readme for the upcoming release!)

SVN:trunk[3037]
2013-12-10 15:47:33 +00:00
Romain Quetiez
cfd2a7baff Readme file updated for the (soon) upcoming release 2.0.2
SVN:trunk[3036]
2013-12-10 15:01:34 +00:00
Romain Quetiez
1867195c25 Portal: Removed the public log for the user request creation form (still available on the ticket details). That was already done in change [2828] for ITIL flavour.
SVN:trunk[3035]
2013-12-10 14:58:22 +00:00
Romain Quetiez
d4bcb9dff8 Regression of 2.0.2 beta: Configure this list + discard all columns: the list cannot be loaded anymore (if saved)
SVN:trunk[3034]
2013-12-10 13:48:32 +00:00
Romain Quetiez
4172cb2023 Readme file updated for the (soon) upcoming release 2.0.2
SVN:trunk[3033]
2013-12-10 12:48:10 +00:00
Denis Flaven
ebff827013 #805 (again): proper fix to avoid blocking when creating a ticket with DBInsert() (instead of DBInsertNoReload !!)
SVN:trunk[3032]
2013-12-10 11:07:24 +00:00
Denis Flaven
1afcf46970 #805: fix the issue when creating tickets from the SOAP web service.
SVN:trunk[3031]
2013-12-10 10:16:40 +00:00
Romain Quetiez
2e37ccc4c2 #770 and #853: When a list is configured directly from the table (NOT from the Dialog box), then these settings are not kept neither when creating a shortcut nor when opening the "configure this list" dialog
SVN:trunk[3030]
2013-12-09 20:38:05 +00:00
Romain Quetiez
3b188524ca #770 Complete the fix implemented in [2990], the configuration is kept even if not saved for the list
SVN:trunk[3029]
2013-12-09 15:55:11 +00:00
Denis Flaven
bd1096b0fc #756: allow incidents and user request to be linked to 'closed' problems.
SVN:trunk[3028]
2013-12-09 12:57:55 +00:00
Denis Flaven
d42443697c Security enhancements:
- ensure that a user can ony see the details of the ticket she/he is allowed to see, even if the id is typed manually
- add a define'd filter to filter the drop-down lists of the search form for searching closed tickets.

SVN:trunk[3027]
2013-12-09 11:39:25 +00:00
Romain Quetiez
8509237084 #851 Ampersand (aka '&') not welcomed in the setup wizard
SVN:trunk[3026]
2013-12-09 10:52:21 +00:00
Romain Quetiez
f16d1ee1e4 PHP Mail transport to allow 100% of recipients in BCC (proposed on github)
SVN:trunk[3025]
2013-12-05 12:06:30 +00:00
Romain Quetiez
5672bee85f Regression introduced with the capability to disable mandatory ext keys (defaults to true)
SVN:trunk[3024]
2013-12-04 16:15:26 +00:00
Denis Flaven
4d6ddb8586 Fixes to the packaging after a first test on CentOS
SVN:trunk[3023]
2013-12-03 15:21:22 +00:00
Denis Flaven
a71b3bc231 Protects the setup in case of missing PHP-JSON module
SVN:trunk[3022]
2013-12-03 11:47:42 +00:00
Romain Quetiez
723d51a871 Readme file updated for the upcoming release
SVN:trunk[3021]
2013-12-03 11:07:59 +00:00
Denis Flaven
4e1c3f321f First step toward a generic Linux packaging supporting both DEBs and RPMs...
SVN:trunk[3020]
2013-12-03 11:04:33 +00:00
Denis Flaven
2b58bca313 Use jQuery.inArray() instead of array.indexOf() to be compatible with IE8
SVN:trunk[3019]
2013-12-03 10:50:38 +00:00
Denis Flaven
9b1d383848 Spanish translation contributed by Miguel Turrubiates
SVN:trunk[3018]
2013-12-03 10:22:30 +00:00
Romain Quetiez
96c1ec42ed Internal: API to build the SQL statements to (re)-create an object
SVN:trunk[3017]
2013-12-03 09:59:42 +00:00
Romain Quetiez
7cb2fb9b02 Internal: new setting to tweak the data model by allowing null for all external keys (use with care!)
SVN:trunk[3016]
2013-12-03 09:58:29 +00:00
Romain Quetiez
6f90d626fc Code refactoring: MakeSelectFilter
SVN:trunk[3015]
2013-12-03 09:56:46 +00:00
Romain Quetiez
62302f9138 Reverted the change made in revision 2999: revision 3000 does protect against the annoying notice... and everything seems working fine... still to be tested with PH >=5.4
SVN:trunk[3014]
2013-12-03 08:42:48 +00:00
Romain Quetiez
278cb653db Fixed regression in the Notifications page (leading to a conflict between the two tabs)
SVN:trunk[3013]
2013-12-03 08:33:54 +00:00
Romain Quetiez
9f9baf9caa Finalized the about box
SVN:trunk[3012]
2013-11-29 16:18:01 +00:00
Denis Flaven
09ebce2587 Transparent background
SVN:trunk[3011]
2013-11-29 16:08:39 +00:00
Denis Flaven
3c8cf0e8fb Bug fix: duplicate ID was used for the VLAN badge
SVN:trunk[3010]
2013-11-29 15:13:52 +00:00
Romain Quetiez
5542cfd79e Module refactoring: Configuration management must not require the installation of a Service Management module
SVN:trunk[3009]
2013-11-29 14:57:34 +00:00
Denis Flaven
60e7c22ab4 #838 following on the bug fix: more standard naming for the parameters of the query
SVN:trunk[3008]
2013-11-29 11:38:51 +00:00
Denis Flaven
92502a7a88 bug fix: Allow Support Agents to perform "wait for approval" on a UserRequest ticket.
SVN:trunk[3007]
2013-11-29 11:28:39 +00:00
Romain Quetiez
47c65b161d Fixed bug with the brand new about box
SVN:trunk[3006]
2013-11-29 11:23:51 +00:00
Romain Quetiez
5f98c0dcab About box - alpha version
SVN:trunk[3005]
2013-11-29 10:50:53 +00:00
Denis Flaven
9e4b25e833 #849: fix for the special case of loading class tags into a hierarchy
SVN:trunk[3004]
2013-11-28 17:15:09 +00:00
Denis Flaven
4b095738d5 #849: fix for the special case of loading class tags into a hierarchy
SVN:trunk[3003]
2013-11-28 16:28:01 +00:00
Romain Quetiez
fa615638d9 Defensive programming: protected against the Notice "array to string conversion" that appears in PHP 5.4 (thus not on every systems)
SVN:trunk[3002]
2013-11-28 09:55:39 +00:00
Romain Quetiez
a24b4437aa #816 Completed the fix (still a Notice)
SVN:trunk[3001]
2013-11-28 09:52:33 +00:00
Romain Quetiez
9f95d951d4 #825 and #830: removed annoying Notices (array to string conversion)
SVN:trunk[3000]
2013-11-28 09:44:21 +00:00
Romain Quetiez
7400bd7dca #816 Suppresses a Notice for PHP >= 5.4, and potentially fixes a bug (no idea on how to produce it)
SVN:trunk[2999]
2013-11-27 13:29:32 +00:00
Romain Quetiez
258b4be167 #830 (continuation: suppressed the notice "array to string conversion"
SVN:trunk[2998]
2013-11-27 09:20:16 +00:00
Denis Flaven
4a849ee4db #764: Saving settings as "Default for all lists" now works as expected
SVN:trunk[2997]
2013-11-27 08:51:51 +00:00
Erwan Taloc
18664c8151 Fix bug #757
SVN:trunk[2996]
2013-11-26 22:00:00 +00:00
Erwan Taloc
85472fe67a Fix bug #837 Remove [+] button on external keys where it's not meaningful
+ button had been removed for all ExternalKey having a complex filter defined

SVN:trunk[2995]
2013-11-26 21:45:54 +00:00
Erwan Taloc
e1087d3f87 Fix bug #835 Remove dependency between ticket and Delivery model
SVN:trunk[2994]
2013-11-26 21:19:11 +00:00
Erwan Taloc
e789c6baec Fix enhancement request #834 Add IP for Virtual Machine
SVN:trunk[2993]
2013-11-26 21:06:56 +00:00
Erwan Taloc
817cc0476a Fix bug #824 Change ranking in CI Overview dashboard
SVN:trunk[2992]
2013-11-26 20:55:59 +00:00
Erwan Taloc
ea5908ac41 Fix bug #836 Adding linkset "applicationsolution_list" in details list
SVN:trunk[2991]
2013-11-26 20:51:48 +00:00
Romain Quetiez
f5d42b95b8 Code cleanup
SVN:trunk[2990]
2013-11-26 15:59:22 +00:00
Romain Quetiez
d7093a9a6f #770 Preserve list configuration (if dedicated) when creating a shortcut
SVN:trunk[2989]
2013-11-26 15:58:05 +00:00
Denis Flaven
7636b987b1 #823: proper figures for the CSV import "confirmation" pie chart.
SVN:trunk[2988]
2013-11-26 14:28:22 +00:00
Romain Quetiez
55f1763b60 #830 ... completes the change 2980 (that accompanied changes 2921 and 2948, for an optimization on bulk operations)
SVN:trunk[2987]
2013-11-26 14:27:24 +00:00
Denis Flaven
87e33c72b5 #775: preserve the open/closed status of the search form during auto-reload... by not reloading the form at all!
SVN:trunk[2986]
2013-11-26 14:19:13 +00:00
Denis Flaven
99695a0fc1 #841: properly select the current tab
SVN:trunk[2985]
2013-11-26 13:54:17 +00:00
Denis Flaven
0aa0de9f1c Make sure that we attach ONLY the elements that are visible in the form when using the "select all" mode for managing 1:n links.
SVN:trunk[2984]
2013-11-26 11:16:11 +00:00
Denis Flaven
ebe89b0af7 #839: Managing n:1 links asked to leave the page (under Chrome!)
SVN:trunk[2983]
2013-11-26 10:39:56 +00:00
Denis Flaven
74f895b5f4 #838: the OQL query for getting the SLTs is now a constant defined in the XML
SVN:trunk[2982]
2013-11-26 10:03:53 +00:00
Romain Quetiez
9bc5406abb #842 Internal: recode the notifications page to allow several types of actions
SVN:trunk[2981]
2013-11-26 09:39:34 +00:00
Romain Quetiez
dd1cf43d41 #830 Regression introduced in the beta. Related to the management of query arguments
SVN:trunk[2980]
2013-11-25 15:10:09 +00:00
Romain Quetiez
b62b9caaf2 #829 Corrupted pwd when attempting to create an account without any profile
SVN:trunk[2979]
2013-11-25 09:01:35 +00:00
Romain Quetiez
36149df584 Extensibility: make sure that checks implemented in an overload of CheckToWrite will be seen when parent::CheckToWrite is invoked at the end of the overload
SVN:trunk[2978]
2013-11-21 16:30:58 +00:00
Romain Quetiez
e48716753d Optimizations: when displaying a "short" list, made of objects having the same status, three additional queries were made (now just one Group By query is made)
SVN:trunk[2977]
2013-11-21 16:15:37 +00:00
Romain Quetiez
8c702a42e9 #827 Default language given at setup not taken into account (login page always in english)
SVN:trunk[2976]
2013-11-21 15:33:09 +00:00
Romain Quetiez
494e559748 Improved the KPI logging: setting log_kpi_duration to 2 will enable blaming of the FIRST caller (callstack entirely shown in the produced report)
SVN:trunk[2975]
2013-11-20 17:03:04 +00:00
Romain Quetiez
a1801e53a2 Got rid of an unnecessary query (responsible for 8% of the time spent in a standard iTop page!!!)
SVN:trunk[2974]
2013-11-20 16:16:19 +00:00
Romain Quetiez
f856859f83 #832 Typos in German (tickets overview)
SVN:trunk[2973]
2013-11-19 13:16:10 +00:00
Denis Flaven
7ebce0a841 Improved version of DoPostRequest which optionally uses cURL to workaround PHP/OpenSSL bugs !
SVN:trunk[2972]
2013-11-07 15:03:01 +00:00
Romain Quetiez
3f50d3ea59 Helper for HTTP POSTs: can return the headers if requested
SVN:trunk[2971]
2013-11-06 15:29:15 +00:00
Denis Flaven
898c235c0d Fix for the validation of the forms in the portal. The (lack of) localization was breaking it!
SVN:trunk[2970]
2013-10-29 16:26:46 +00:00
Romain Quetiez
85e261a5fa Added user rights for VLAN and related classes
SVN:trunk[2969]
2013-10-29 16:01:22 +00:00
Romain Quetiez
d912e7f4fb #804 (Continued) Fix for GetHilightClass (previous revision: 2952) was not sufficient + applied the same fix for GetIcon!
SVN:trunk[2968]
2013-10-29 15:55:00 +00:00
Romain Quetiez
bc14ad9e80 Datamodel version is 2.0.2 !
SVN:trunk[2967]
2013-10-29 14:44:27 +00:00
Romain Quetiez
e81d872306 Added missing cosmetic bitmap for the portal
SVN:trunk[2966]
2013-10-29 13:54:34 +00:00
Romain Quetiez
f83bb7fa90 Fixed regression introduced with "forgot password": button to reset the user password labelled as "Send now!"
SVN:trunk[2965]
2013-10-29 13:13:58 +00:00
Romain Quetiez
032947ff03 Portal with Incident tickets: added rights on Incident for Portal users
SVN:trunk[2964]
2013-10-29 11:37:04 +00:00
Romain Quetiez
9e39013d4c Portal: service type mapping can be an empty string
SVN:trunk[2963]
2013-10-29 11:00:31 +00:00
Romain Quetiez
5d02db5440 Show the new setting portal_tickets in the default config file
SVN:trunk[2962]
2013-10-29 09:36:23 +00:00
Romain Quetiez
7300698240 Update of the readme with the latest changes
SVN:trunk[2961]
2013-10-29 09:16:29 +00:00
Erwan Taloc
a47bbb3a9a Add a version for Documents
SVN:trunk[2960]
2013-10-28 18:51:39 +00:00
Romain Quetiez
a333bcb084 User portal: enable the creation of Incident tickets (ITIL + requires a change in the configuration file -see the readme file)
SVN:trunk[2959]
2013-10-28 16:50:13 +00:00
Romain Quetiez
e92d193347 Draft of the readme for 2.0.2 beta
SVN:trunk[2958]
2013-10-25 12:06:04 +00:00
Denis Flaven
d5a0808118 Cosmetic style adjustment for the portal...
SVN:trunk[2957]
2013-10-25 09:38:13 +00:00
Denis Flaven
864ce74cbc #805 The label of the ticket must be computed at the last minute, just before insertion, for the Mutex to be effective
SVN:trunk[2956]
2013-10-24 12:05:16 +00:00
Romain Quetiez
f684cb1745 Compiler: added "constants"
SVN:trunk[2955]
2013-10-24 09:49:56 +00:00
Denis Flaven
54769aa2d1 Oops, rollback of the previous (unexpected) commit...
SVN:trunk[2954]
2013-10-24 09:28:25 +00:00
Denis Flaven
272a249d14 #805 Use a mutex to turn the insertion of a new ticket into an atomic operation
SVN:trunk[2953]
2013-10-24 09:15:41 +00:00
Denis Flaven
02e6658439 #804 tickets' highlighting is now based on the computation performed by the stopwatch, in order to support non 24x7 working hours
SVN:trunk[2952]
2013-10-24 08:11:40 +00:00
Romain Quetiez
0c327f2c36 Added a demo mode (config: demo_mode = true). In that mode, logins get read-only (even for admins)
SVN:trunk[2951]
2013-10-23 13:36:44 +00:00
Romain Quetiez
dcb5a7208a #800 No need to track that last update has changed each time the ticket gets updated (common to all types of tickets)
SVN:trunk[2950]
2013-10-22 14:05:39 +00:00
Denis Flaven
94de069963 Fixed a regression in the computation of default choices.
SVN:trunk[2949]
2013-10-22 12:52:45 +00:00
Romain Quetiez
f0c66be7cd #783, #233 and #466 The recent revision (2921 for ticket #783) introduced a significant slow down when performing CSV import (but not only). This new revision does suppress the regression, and even speeds up bulk updates in general. This revision is candidate for retrofit into branch 2.0.1 (along with 2921)
SVN:trunk[2948]
2013-10-22 12:46:09 +00:00
Denis Flaven
0b7ed90e18 Fixed a regression introduced by revision [2856]: Avoid breaking pages with tabs when there is no BASE tag at all...
SVN:trunk[2947]
2013-10-22 11:49:38 +00:00
Romain Quetiez
20ba6242e7 No time limit for long operations like: Bulk delete, CSV import (interactive) and Bulk modify
SVN:trunk[2946]
2013-10-22 08:09:34 +00:00
Erwan Taloc
015919702b Change translation of incident menu in French dictionary
SVN:trunk[2945]
2013-10-21 13:15:27 +00:00
Erwan Taloc
ae8ff6b675 Fix Trac #796
Prevent Support agent to create ticket for obsolete Services and Service sub categories

SVN:trunk[2944]
2013-10-21 13:06:10 +00:00
Erwan Taloc
0ea6657610 Add VLANs on Subnet and Physical Interfaces
SVN:trunk[2943]
2013-10-21 12:35:57 +00:00
Denis Flaven
9d6d93d42f #757 Better UI to manage direct linksets: added the ability to provide the "reverse query" by specifying a '<filter>' tag on AttributeLinkedSet.
SVN:trunk[2942]
2013-10-21 12:25:07 +00:00
Denis Flaven
4f845ec98d #780 Dictionary entries for "Check All" and "Uncheck All"
SVN:trunk[2941]
2013-10-21 09:08:32 +00:00
Erwan Taloc
f65c84300f Change translation in French dictionary
SVN:trunk[2940]
2013-10-21 07:07:19 +00:00
Erwan Taloc
d8b9679346 Change translation in French dictionary
SVN:trunk[2939]
2013-10-21 06:59:56 +00:00
Erwan Taloc
e090b866e1 Change translation in Franch dictionnary
SVN:trunk[2938]
2013-10-21 06:59:20 +00:00
Erwan Taloc
d30f34afc1 Translate menu entry in French
SVN:trunk[2937]
2013-10-21 06:57:51 +00:00
Erwan Taloc
3b7aa49ca3 Translate menu entry in French
SVN:trunk[2936]
2013-10-21 06:57:26 +00:00
Erwan Taloc
441bd44f97 Hidde unused attribute end end in the search form
SVN:trunk[2935]
2013-10-21 06:46:49 +00:00
Erwan Taloc
7a18730949 Hidde unused attribute end end in the search form
SVN:trunk[2934]
2013-10-21 06:46:17 +00:00
Erwan Taloc
0e27be0aca Hidde unused attribute end end in the search form
SVN:trunk[2933]
2013-10-21 06:45:37 +00:00
Erwan Taloc
1c16365881 Remove duplicate display of attribute service provider
SVN:trunk[2932]
2013-10-21 06:44:53 +00:00
Erwan Taloc
585e06f096 Remove duplicate display of attribute service provider
SVN:trunk[2931]
2013-10-21 06:44:24 +00:00
Romain Quetiez
edce93282b #792 Duplicate entries in the parent/child tickets when updating the case log and applying a stimulus (e.g. Close the WO) at the same time.
SVN:trunk[2930]
2013-10-18 15:54:54 +00:00
Romain Quetiez
26dca89b19 #780 Auto refresh for the dashboards (+ cosmetics changes for the shortcuts)
SVN:trunk[2929]
2013-10-18 14:14:48 +00:00
Denis Flaven
9b58e736ff #787 Added buttons to check/uncheck all options at once in multi selects inside search forms.
SVN:trunk[2928]
2013-10-18 09:06:25 +00:00
Denis Flaven
36e6a6106b #757 Better UI to manage direct linksets... on going...
SVN:trunk[2927]
2013-10-18 08:26:31 +00:00
Romain Quetiez
bbb31e2b7f #780 Auto refresh for the shortcuts
SVN:trunk[2926]
2013-10-17 14:47:05 +00:00
Romain Quetiez
afa3c40c3e Improved the column load optimization which was causing object reloads in various circumstances (impact can be important when loading lists with many lines):
1) When changing column settings (menu "Configure this list")
2) When a plugin was systematically reading some data (e.g. Highlighting late tickets requires to read tto/trr/status/start_date!)

SVN:trunk[2925]
2013-10-17 09:46:10 +00:00
Romain Quetiez
401d61aa76 Creation of indexes on several columns (exploited for a few classes when it was obvious)
SVN:trunk[2924]
2013-10-16 15:21:20 +00:00
Romain Quetiez
eda203af26 #785 Share the results of a query phrase (preview of the results in the query details page -iif it has NO parameter)
SVN:trunk[2923]
2013-10-16 11:45:17 +00:00
Romain Quetiez
3022ba9b1a Fixed regression introduced a few months ago when reworking the dashboard edition (after the release of 2.0.1)
SVN:trunk[2922]
2013-10-16 11:43:21 +00:00
Romain Quetiez
440f50259b #783 Added the placeholder $this->xxx_list$ for emailing (names separated by a new line, truncated to 100 items)
SVN:trunk[2921]
2013-10-16 10:36:15 +00:00
Romain Quetiez
5d402a5f9d Reviewed the instrumentation to help in tuning the performance (added a message in the admin banner when logging is active)
SVN:trunk[2920]
2013-10-16 10:33:30 +00:00
Denis Flaven
2d83f331e2 #771: better display for "edit in place".
SVN:trunk[2919]
2013-10-16 09:34:48 +00:00
Denis Flaven
677cc2b19e #795 Issue when using the actual (id) value of an external key as a reconciliation field
SVN:trunk[2918]
2013-10-16 08:54:22 +00:00
Romain Quetiez
caa621eb04 Reviewed the instrumentation to help in tuning the performance
SVN:trunk[2916]
2013-10-15 16:08:43 +00:00
Denis Flaven
5ea2ac3fef #741 Complete localization of the CSV import confirmation dialog
SVN:trunk[2915]
2013-10-15 16:00:51 +00:00
Denis Flaven
09318b81c0 #790 Only report as installed the modules from the previous installation, not all previous installations.
SVN:trunk[2914]
2013-10-15 15:17:50 +00:00
Denis Flaven
d5be250640 #754 Prevent UserRequests to have themselves set as Parent Request (and same for Incidents)
SVN:trunk[2913]
2013-10-15 13:41:15 +00:00
Denis Flaven
fca3bb2a73 #738 Adding a space at the end of the mailto: URL to make it better recognized by mail clients (namely Outlook)
SVN:trunk[2911]
2013-10-15 08:09:15 +00:00
Denis Flaven
bf9cb67226 #791 Protect against single quotes in localized strings...
SVN:trunk[2909]
2013-10-14 16:19:03 +00:00
Denis Flaven
e54d6ecc12 #777 mandatory fields that are external keys are now displayed with a star before the arrow: ExtkeyName*->ReconciliationField. In import the old syntax is supported as well.
SVN:trunk[2908]
2013-10-14 15:36:49 +00:00
Romain Quetiez
30de6a1e39 Instrumented the code to measure the impact of object reloads
SVN:trunk[2907]
2013-10-14 15:19:26 +00:00
Romain Quetiez
6de4d93ef2 #562 and #760 Refresh of the german translation
SVN:trunk[2906]
2013-10-14 15:10:29 +00:00
Romain Quetiez
3c3d4a073d #769 Title of pies and charts are not consistent with the title of other dashlets
SVN:trunk[2905]
2013-10-14 14:48:09 +00:00
Romain Quetiez
c2efdfa0bb #794 Could not export the field friendlyname in format 'spreadsheet'
SVN:trunk[2903]
2013-10-14 14:17:07 +00:00
Romain Quetiez
2218003bec #758 REST service: key given in clear in the returned objects (incremented the API verion to 1.1)
SVN:trunk[2902]
2013-10-14 13:52:11 +00:00
Denis Flaven
3ffd289a5e #793 provide the default '=' and '!=' operators for all types of Computed Fields.
SVN:trunk[2901]
2013-10-14 13:49:21 +00:00
Romain Quetiez
fe4d55fbf6 Missing french translation
SVN:trunk[2900]
2013-10-14 13:44:50 +00:00
Romain Quetiez
b5d9e5a8b6 #773 Display boolean values from the stop watches as yes/no (localized, like enums) + took the opportunity to enable the export in spreadsheet format
SVN:trunk[2899]
2013-10-14 13:22:52 +00:00
Romain Quetiez
046a7b0e2d Fixed a regression due to the change [2877] Tooltip to preview attachments
SVN:trunk[2898]
2013-10-14 12:15:07 +00:00
Erwan Taloc
32ca9727f7 Fix Bug #762 : Remove wrong fields approval_date approval_comments for a Routine change
SVN:trunk[2896]
2013-10-11 11:42:22 +00:00
Erwan Taloc
08fc696f94 Modify Sample data for Service categories to set them to status "production" by default
SVN:trunk[2895]
2013-10-11 11:13:13 +00:00
Erwan Taloc
39ef3d13e6 Fix bug #768 to avoid to select obsolete service and service sub categories in the portal
SVN:trunk[2894]
2013-10-11 11:05:45 +00:00
Erwan Taloc
151b300856 Fix bug #789 to add up to 12 Digit for a IPInterface
SVN:trunk[2893]
2013-10-11 10:51:51 +00:00
Erwan Taloc
c5bf962095 Fix bug #755 to prevent modification of CIs and Contacts list for UserRequest and Incidents
SVN:trunk[2892]
2013-10-11 10:47:14 +00:00
Erwan Taloc
a6a4cf5d00 Bug #742 fixed. Allow portal user to modify a ticket when it is pending
SVN:trunk[2891]
2013-10-11 10:34:47 +00:00
Erwan Taloc
90f7aa04bb Fib bug #739 preventing a Support Agent to set a UserRequest to status "Pending"
SVN:trunk[2890]
2013-10-11 10:33:32 +00:00
Romain Quetiez
bb9f074670 Show all types of Actions from the "Notifications/Actions" tab.
SVN:trunk[2889]
2013-10-11 10:30:29 +00:00
Erwan Taloc
ef26f395bd Fix bug #751. Check that class Logical Volume exists when checking dependencies of a Server
Add attribute Subnet name on Subnet element

SVN:trunk[2888]
2013-10-11 10:24:32 +00:00
Denis Flaven
e34516745c Retrofit the useful DoPostRequest function which was used (and defined) in several extensions.
SVN:trunk[2886]
2013-10-11 08:38:38 +00:00
Erwan Taloc
8474b423fe Move definition of the delivery model of an organization from itop-config-mgmt to itop-service-mgmt module.
SVN:trunk[2885]
2013-10-11 08:36:26 +00:00
Denis Flaven
30b2d93bdf Added support of different (sub)classes of notifications in the "Notifications" tab on an object.
SVN:trunk[2884]
2013-10-10 16:01:44 +00:00
Denis Flaven
7162db0487 Fix for properly computing the default choices in case of upgrade...
SVN:trunk[2883]
2013-10-09 10:23:40 +00:00
Romain Quetiez
e08fa6b43b #745 Default menu is not computed correctly (depends on the customizations made to the menu -> order of declaration)
SVN:trunk[2882]
2013-10-08 14:27:27 +00:00
Denis Flaven
4b9e6edab8 File identifiers are no longer numeric only !
SVN:trunk[2881]
2013-10-08 12:46:37 +00:00
Romain Quetiez
7017bbf88b The login web page must NOT be cached by the web browsers
SVN:trunk[2880]
2013-10-08 08:28:25 +00:00
Romain Quetiez
4f4ceeadc6 Removed calls to console.log
SVN:trunk[2879]
2013-10-08 08:23:38 +00:00
Romain Quetiez
b0ecb2f6c6 #774 Sort the enums in the selection drop-down box (search forms) -initially based on the declaration order
SVN:trunk[2878]
2013-10-08 07:34:59 +00:00
Denis Flaven
4be0837ead #782: preview (as a tooltip) for image attachments.
SVN:trunk[2877]
2013-10-03 16:53:25 +00:00
Romain Quetiez
e3832a13a6 #784 Data sync: display the attribute code (as well as its label in the user language)
SVN:trunk[2876]
2013-10-03 15:50:56 +00:00
Romain Quetiez
169f576ccf #781 Plain text dashlet shown on one single line
SVN:trunk[2875]
2013-10-03 15:40:45 +00:00
Romain Quetiez
894b59eee1 #779 It is possible to record a wrong OQL in the phrase book, but then it cannot be edited anymore!
SVN:trunk[2874]
2013-10-03 15:30:29 +00:00
Denis Flaven
fe41d09acb Support for input values with no icon.
SVN:trunk[2873]
2013-10-02 14:34:45 +00:00
Denis Flaven
387e4c6f0b Upload image button should not submit the parent form !!!
SVN:trunk[2872]
2013-10-02 12:48:22 +00:00
Romain Quetiez
6f8be14711 Internal: failed authentication to return error 401 instead of prompting the end-user (to be exploited by the ajax calls)
SVN:trunk[2871]
2013-10-02 09:30:14 +00:00
Denis Flaven
7d824dd03c Removed an obsolete comment...
SVN:trunk[2869]
2013-09-30 14:41:08 +00:00
Denis Flaven
899a7c1ba0 New pattern accepting the new global Top Level Domains (gTLD)
SVN:trunk[2867]
2013-09-27 07:29:15 +00:00
Romain Quetiez
a84eff5c3b Fixed regression introduced on fix [2829]
SVN:trunk[2866]
2013-09-26 15:32:16 +00:00
Romain Quetiez
e0ae6484d3 Fixed regression on the change on change tracking (sic!)
SVN:trunk[2865]
2013-09-26 15:22:23 +00:00
Romain Quetiez
552e90f674 Logoff: display the message in the user language (used to be 100% english)
SVN:trunk[2864]
2013-09-25 10:20:33 +00:00
Romain Quetiez
898ee016c9 Generalized the option tracking_level to any kind of attributes. Defaults to 'all', can be set to 'none' to disable the change tracking on a single attribute (LinkSets still have the same allowed values: none, list, details and all).
SVN:trunk[2863]
2013-09-25 09:47:50 +00:00
Denis Flaven
7d87aad0bb Protect the deletion of objects with very long friendly names
SVN:trunk[2861]
2013-09-24 16:19:22 +00:00
Romain Quetiez
8d068b6a93 #767 JSON/REST Reconciliations made on loose criteria (forced to strict equality, no way to specify a loose criteria)
SVN:trunk[2860]
2013-09-24 13:38:49 +00:00
Romain Quetiez
ea36d6b147 New feature: Forgot password (prerequisite in the very standard authent local module)
SVN:trunk[2859]
2013-09-24 12:48:09 +00:00
Romain Quetiez
90e024b2bb Cosmetics on the login web page (2 of 2!)
SVN:trunk[2858]
2013-09-24 12:44:31 +00:00
Romain Quetiez
955beb70e4 Cosmetics on the login web page
SVN:trunk[2857]
2013-09-24 12:43:44 +00:00
Denis Flaven
1a60b7005b Avoid breaking pages with tabs when there is no BASE tag at all...
SVN:trunk[2856]
2013-09-24 10:05:33 +00:00
Romain Quetiez
fde3808cdf New feature: Forgot password -> email to reset (possibly disabled in the config file)
SVN:trunk[2855]
2013-09-24 09:15:52 +00:00
Denis Flaven
76e0ee66ae Allow for comparisons of the module's versions in the expression of dependencies. For example one can now say "itop-config-mgmt/>=2.0.2" for a dependency.
SVN:trunk[2853]
2013-09-23 12:48:55 +00:00
Romain Quetiez
a2a0ee5194 Fixed bug in the JSON REST API: core/create and core/update, could not reset an external key (0)
SVN:trunk[2852]
2013-09-19 11:42:06 +00:00
Romain Quetiez
0bced2f9ae ModelFactory: needed / define_if_not_exists were not equivalent
SVN:trunk[2850]
2013-09-12 08:09:15 +00:00
Romain Quetiez
ccc9729cc5 #763 Could not use "configure this list" once a stop watch has been added to the list, which is a pitty because such attributes are not aimed at being displayed in lists!
SVN:trunk[2848]
2013-09-11 09:53:31 +00:00
Romain Quetiez
cf383bcf6b Fixed bug (wrong DB charset after invoking AnalyzeInstallation!)
SVN:trunk[2847]
2013-09-06 15:18:41 +00:00
Romain Quetiez
9292d5fa33 Icon select: load images when the control becomes visible
SVN:trunk[2846]
2013-09-05 16:01:07 +00:00
Denis Flaven
3b6646f1b9 Load structural data for all selected modules indepentyl of:
- the load of sample data
- first install or upgrade

SVN:trunk[2845]
2013-09-03 15:37:10 +00:00
Romain Quetiez
ca1d4d8936 Management of environments: the banner must be injected by the mean of iPageUIExtension
SVN:trunk[2844]
2013-09-02 12:54:00 +00:00
Romain Quetiez
afa6399dce Regression due to the safe compile: fixed an issue depending on the OS: could not compile (bug with PHP rename)
SVN:trunk[2843]
2013-09-02 08:52:14 +00:00
Romain Quetiez
f93b1e1c1c Module installation information always loaded within the meta model
SVN:trunk[2842]
2013-08-30 14:20:20 +00:00
Denis Flaven
fd7adb2202 Make the logo transparent (background removal)
SVN:trunk[2841]
2013-08-30 08:30:28 +00:00
Denis Flaven
05f50c285c Fixed an "Undefined variable" error
SVN:trunk[2840]
2013-08-30 08:17:22 +00:00
Romain Quetiez
0aa2dc9ce3 Portal: cosmetics (same logo as the application logo)
SVN:trunk[2839]
2013-08-30 07:38:07 +00:00
Romain Quetiez
607236a7cb Compiler: added brand management
SVN:trunk[2838]
2013-08-29 08:35:44 +00:00
Romain Quetiez
564ba105eb CRON: report that CRON is already running BEFORE saying that the DB is read-only (re-entrance during an operation done in the background)
SVN:trunk[2837]
2013-08-27 14:20:12 +00:00
Romain Quetiez
73b492e892 New mechanism: a module page can be accessed by the mean of a canonical URL (utils::GetAbsoluteUrlModulePage to build the proper URL)
SVN:trunk[2836]
2013-08-27 14:04:59 +00:00
Romain Quetiez
e99d96e081 Safe compilation (works in a temporary directory, on success then move it into env-production)
SVN:trunk[2835]
2013-08-27 12:19:55 +00:00
Romain Quetiez
abae2129ad CRON: protection against re-entrance now relies on a bullet-proof mutex. Also added the option 'debug=1' to output the call stack in case an exception occurs (not always because of passwords being shown in the call stack)
SVN:trunk[2834]
2013-08-26 15:29:32 +00:00
Romain Quetiez
f8c3e0ddea Model factory: fixed two bugs
SVN:trunk[2833]
2013-08-23 14:37:43 +00:00
Romain Quetiez
a28a0aba7d Compiler: when creating a test environment, take the relevant delta file (source env != dest env)
SVN:trunk[2832]
2013-08-23 14:36:33 +00:00
Romain Quetiez
358911604b #752 Notifications sent several times (or too late) when MySQL is hosted on another server
SVN:trunk[2831]
2013-08-23 07:43:10 +00:00
Romain Quetiez
75eb44912f Setup: Source dir recorded with a trailing backslash under windows
SVN:trunk[2829]
2013-08-22 11:57:31 +00:00
Romain Quetiez
2893d16d58 Restored the behavior of itop-sla-computation (if present, then it becomes the default working hour computer)
SVN:trunk[2828]
2013-08-21 08:07:31 +00:00
Romain Quetiez
2dbcb6d416 Restored the logo on the portal (picture with transparent background)
SVN:trunk[2827]
2013-08-20 12:11:53 +00:00
Romain Quetiez
2b4ad2c50b Reviewed the portal (look and feel slightly improved)
SVN:trunk[2826]
2013-08-20 11:42:46 +00:00
Romain Quetiez
7e4b69d272 Improved the error reporting for the backup (in case mysqldump fails with a single error, then the error is displayed directly)
SVN:trunk[2825]
2013-08-19 15:16:32 +00:00
Romain Quetiez
d8c9044e15 Improved the error reporting for the backup (in case mysqldump fails with a single error, then the error is displayed directly)
SVN:trunk[2824]
2013-08-19 15:15:53 +00:00
Denis Flaven
b2e4cf2c09 Add a carriage return to the error message output when iTop is NOT yet installed.
SVN:trunk[2823]
2013-08-19 14:59:41 +00:00
Romain Quetiez
08fa8362e3 CRON: reschedule at startup IIF the task is inactive or it is planned in the future
SVN:trunk[2822]
2013-08-14 15:01:53 +00:00
Romain Quetiez
447736f585 CRON to exit gracefully if iTop not yet installed
SVN:trunk[2821]
2013-08-14 13:21:41 +00:00
Denis Flaven
5ed91c2223 New verb "AfterDatabaseSetup" for performing installation tasks after the completion of the DB creation (+predefined objects & admin account)
SVN:trunk[2820]
2013-08-14 07:34:07 +00:00
Romain Quetiez
4fa07536d5 Added datamodel delta (if any) to the backup file
SVN:trunk[2819]
2013-08-13 09:01:04 +00:00
Romain Quetiez
8881450d59 Delta revision id can be stored into the XML delta
SVN:trunk[2818]
2013-08-09 15:53:42 +00:00
Romain Quetiez
58af5528be Possibility to introduce a delta (not in a module) at compile time
SVN:trunk[2817]
2013-08-09 15:47:05 +00:00
Romain Quetiez
98a1242050 New capability for CRON: handle tasks scheduled at given date/time (as opposed to a task being executed more or less continuously).
SVN:trunk[2816]
2013-08-08 15:23:05 +00:00
Denis Flaven
9536c99422 Allow "Support Agents" to put an Incident in "Pending" state.
SVN:trunk[2814]
2013-08-01 08:24:19 +00:00
Denis Flaven
7cfd5ad2a3 Ugly fix for a nasty change in jQuery UI behavior: UI tabs were considered as "Ajax" tabs when the page has a "base" tag, which was not the case in previous versions. Cf http://bugs.jqueryui.com/ticket/8637
SVN:trunk[2813]
2013-07-31 16:45:33 +00:00
Denis Flaven
86ba340204 #747: protects against the non-existence of the UserRequest class (which is not always installed).
SVN:trunk[2811]
2013-07-30 16:24:52 +00:00
Denis Flaven
b32a142e14 Use the minified version of jquery-migrate, since the non-minified version (which produces debug traces) is excluded from the build.
SVN:trunk[2810]
2013-07-25 09:48:58 +00:00
Denis Flaven
7e45f34a86 #746 allow adding an AttributeBlob with is_null_allowed = true to an existing Data Model. (same issue fixed also for AttributeOneWayPassword).
SVN:trunk[2808]
2013-07-25 09:17:16 +00:00
Denis Flaven
1064feaa8e Properly handle nested forms in "PropertySheet" and "read-only" mode
SVN:trunk[2807]
2013-07-24 17:01:25 +00:00
Denis Flaven
17658d1b6a Bug fix: validation was broken when the first fields were not Ok.
SVN:trunk[2806]
2013-07-24 17:00:30 +00:00
Denis Flaven
ce643d9086 Export the content of the CaseLogs in "spreadsheet" format, with some tricks to preserve the formatting in Excel.
SVN:trunk[2804]
2013-07-17 16:54:27 +00:00
Denis Flaven
481515b419 IsInDefinition is needed => made it public
SVN:trunk[2803]
2013-07-17 14:48:10 +00:00
Denis Flaven
6d60d92b03 Fix for a non localized message.
SVN:trunk[2802]
2013-07-17 08:46:50 +00:00
Denis Flaven
3edbdf76f3 Fix for a non localized message.
SVN:trunk[2801]
2013-07-17 08:46:04 +00:00
Denis Flaven
80bac5275c Forms enhancements:
- The current value of a field is automatically excluded from the forbidden values


SVN:trunk[2800]
2013-07-16 09:16:54 +00:00
Denis Flaven
59fc9e24d9 Forms enhancements:
- The current value of a field is automatically excluded from the forbidden values
- Several levels of subforms can be nested, even when displaying as a property sheet
- Sortables fields re-implemented based on a widget.

SVN:trunk[2799]
2013-07-16 09:16:12 +00:00
Denis Flaven
7db7c0781f Make the portal (slightly) more configurable...
SVN:trunk[2797]
2013-07-12 13:55:28 +00:00
Romain Quetiez
358ddf6019 Fixed issue for the toolkit
SVN:trunk[2796]
2013-07-10 08:48:08 +00:00
Romain Quetiez
ebf08345af Forms: added the possibility to specify forbidden values + message to explain the issue(toolip) (fiwed a bug on the previous implementation, causing a javascript error, hence a stopper regression due to missing event binds)
SVN:trunk[2795]
2013-07-09 09:09:49 +00:00
Denis Flaven
a9ad236439 Allow filtering the Delta output...
SVN:trunk[2794]
2013-07-08 08:48:03 +00:00
Romain Quetiez
3066240ca0 Forms: added the possibility to specify forbidden values + message to explain the issue(toolip)
SVN:trunk[2793]
2013-07-08 08:25:13 +00:00
Romain Quetiez
d82326bfd4 Form as a dialog, possibility to specify an introduction message (cosmetics)
SVN:trunk[2792]
2013-07-05 13:57:07 +00:00
Romain Quetiez
f99ecb40d0 Form as a dialog, possibility to specify an introduction message
SVN:trunk[2791]
2013-07-05 13:54:16 +00:00
Romain Quetiez
d7124123e9 Compiler: allow to set the flags enable_class/enable_action etc. for a TemplateMenuNode (already taken into account at runtime)
SVN:trunk[2790]
2013-07-04 14:17:23 +00:00
Romain Quetiez
e517f2b6f5 Cosmetics on the dashboards
SVN:trunk[2789]
2013-07-03 15:52:31 +00:00
Denis Flaven
ea686059b6 Protect against non existing reconciliation keys...
SVN:trunk[2788]
2013-07-03 09:47:44 +00:00
Denis Flaven
3898371d44 Added support of CSS classes for styling the form
SVN:trunk[2787]
2013-07-03 09:46:16 +00:00
Romain Quetiez
f0a5a0a948 Completed the move of dashboards from separate definition files (e.g. overview.xml) into data model files (8 dashboards were concerned on the model 2.x, 6 for the model 1.x)
SVN:trunk[2786]
2013-07-02 09:18:40 +00:00
Denis Flaven
24ab96769a Re-position the popup menu each time the button is clicked, in case the button was moved...
SVN:trunk[2785]
2013-06-28 09:39:57 +00:00
Romain Quetiez
5e3a34d425 Updated the licences: 2012 -> 2013
SVN:trunk[2784]
2013-06-28 07:19:55 +00:00
Romain Quetiez
76724225e0 XML format: missing/wrong id on dashboards cells
SVN:trunk[2783]
2013-06-28 07:11:24 +00:00
Romain Quetiez
3ab539e2ba Dashboard re-engineering
SVN:trunk[2782]
2013-06-27 15:21:35 +00:00
Romain Quetiez
721a654152 Fixed regression due to the update of JQuery UI
SVN:trunk[2781]
2013-06-26 16:25:53 +00:00
Denis Flaven
57e51e44f1 Make sure that tabs (and tab panels) are properly identified
SVN:trunk[2780]
2013-06-26 13:56:24 +00:00
Denis Flaven
f7642283f3 Forget about IE 8 support ???
SVN:trunk[2779]
2013-06-26 13:55:27 +00:00
Denis Flaven
e7897b9139 Update for jQuery UI 1.10
SVN:trunk[2778]
2013-06-26 13:54:52 +00:00
Romain Quetiez
59ce84f7cb Cosmetics on the portal
SVN:trunk[2776]
2013-06-14 15:37:47 +00:00
Romain Quetiez
bb6d87e8ed Cosmetics on the portal
SVN:trunk[2775]
2013-06-14 15:15:13 +00:00
Romain Quetiez
46dae2f06f Cosmetics: User portal - added a title (usually, this would appear on the tab of your browser)
SVN:trunk[2774]
2013-06-14 15:05:03 +00:00
Romain Quetiez
0c1a366c07 OQL normalization and dashlets have been made independent from the class MetaModel (adjusted the API)
SVN:trunk[2773]
2013-06-13 14:11:13 +00:00
Romain Quetiez
71cc6f7e6b OQL normalization and dashlets have been made independent from the class MetaModel (reviewed the API)
SVN:trunk[2772]
2013-06-12 07:21:11 +00:00
Romain Quetiez
ba9a50b6fb #736 Could not delete objects unless you are authorized to bulk delete
SVN:trunk[2769]
2013-06-07 07:28:31 +00:00
Romain Quetiez
9ef41a37b8 OQL normalization and dashlets have been made independent from the class MetaModel (2 of 2!)
SVN:trunk[2768]
2013-06-03 13:49:51 +00:00
Romain Quetiez
26db86beb2 OQL normalization and dashlets have been made independent from the class MetaModel
Added OQL normalization unit tests (to be run on a standard installation)

SVN:trunk[2767]
2013-06-03 13:26:14 +00:00
Denis Flaven
69c37b07de Upgrade to jQuery 1.10 and jQuery UI 1.10
SVN:trunk[2766]
2013-05-30 09:48:59 +00:00
Denis Flaven
7844db0719 Upgrade to jQuery 1.10 and jQuery UI 1.10
SVN:trunk[2765]
2013-05-30 09:13:43 +00:00
Denis Flaven
6edb1e3482 Removed the use of the obsolete $.browser property, since we don't care about IE 7 anyway.
SVN:trunk[2764]
2013-05-30 08:11:12 +00:00
Denis Flaven
7b887f3ea5 #734 Fixed a regression on reconciliation keys during CSV import.
SVN:trunk[2761]
2013-05-29 08:53:07 +00:00
Denis Flaven
2fe407967b Preparing for the 2.0.1 release... update of the readme file.
SVN:trunk[2758]
2013-05-22 09:55:29 +00:00
Denis Flaven
703be73c95 #689: Logoff / Change Pwd buttons not visible in the portal if the window is too small: now allow line wrapping...
SVN:trunk[2757]
2013-05-22 09:43:26 +00:00
Denis Flaven
3060462edc #732: Change password: exit after building the page in case of wrong "old" password
SVN:trunk[2756]
2013-05-22 08:43:48 +00:00
Romain Quetiez
263acaf4e4 Missing localized label for some problem management menus, when the Known Error Management module is installed without the Problem Management module
SVN:trunk[2755]
2013-05-21 13:13:44 +00:00
Denis Flaven
db1be8f500 Add "Employee number" to the list of possible reconciliation keys for the class Person.
SVN:trunk[2754]
2013-05-21 11:04:01 +00:00
Romain Quetiez
452eca5288 The ticket class must be abstract!
SVN:trunk[2753]
2013-05-21 10:49:22 +00:00
Romain Quetiez
a728dfcf48 All the overloads of DisplayBareProperties must return an array (otherwise a warning is issued and field validation will not work as expected)
SVN:trunk[2752]
2013-05-21 10:44:13 +00:00
Denis Flaven
2027dc4a3d Make icons transparent...
SVN:trunk[2751]
2013-05-17 12:12:19 +00:00
Denis Flaven
1bc4e1431c Preparing for the 2.0.1 release... update of the readme file.
SVN:trunk[2750]
2013-05-17 08:34:42 +00:00
Denis Flaven
9afe28be20 Support non scalar posted parameters...
SVN:trunk[2749]
2013-05-16 15:45:57 +00:00
Denis Flaven
8073120351 JS syntax cleanup...
SVN:trunk[2748]
2013-05-16 15:44:05 +00:00
Denis Flaven
86c5b3e258 Typo...
SVN:trunk[2747]
2013-05-16 15:42:34 +00:00
Denis Flaven
b971faecda Cleanup of the log output. No need for such verbosity now that we are approaching the release.
SVN:trunk[2746]
2013-05-16 15:41:52 +00:00
Denis Flaven
2708b0de0e #728: the ticket's title is now an (external) attribute of the link between a ticket and a CI.
SVN:trunk[2745]
2013-05-16 14:28:23 +00:00
Romain Quetiez
8dd9893202 Readme: Documented the known issue #730 Leaving the backup temp files on disk
SVN:trunk[2744]
2013-05-16 14:23:35 +00:00
Denis Flaven
48d740da25 #727: prevent a crash in cron.php
SVN:trunk[2743]
2013-05-16 14:05:38 +00:00
Romain Quetiez
7ba5526fda #725 Specify a port for mySQL server
#729 Backup fails on Windows IIS

SVN:trunk[2742]
2013-05-16 13:57:52 +00:00
Denis Flaven
58dfa3335a Spanish localization enhancements, thanks to Miguel Turrubiates
SVN:trunk[2741]
2013-05-16 12:41:10 +00:00
Denis Flaven
deec1aa2a2 Fix the auto-resize (fit) of the dialog's content.
SVN:trunk[2740]
2013-05-16 12:13:18 +00:00
Romain Quetiez
a62c1946a6 #721 Unmet dependencies not detected
SVN:trunk[2739]
2013-05-16 08:54:50 +00:00
Romain Quetiez
a194308486 #726 Missing string in german dictionary
SVN:trunk[2738]
2013-05-16 07:41:36 +00:00
Denis Flaven
2e442dbaa0 #704: preserve the content of a "linkedset" when changing the initial state of the object being created !
SVN:trunk[2737]
2013-05-15 15:56:42 +00:00
Romain Quetiez
ad9ed96960 #673 Could not create a physical interface with default value for the speed (+ definitive corruption of the DB for the device on which the interface is being created!)
SVN:trunk[2735]
2013-05-15 10:23:06 +00:00
Romain Quetiez
efc3b4df07 Silently discard unknown attributes in object templates (in case some modules have not been installed)
SVN:trunk[2733]
2013-05-15 10:18:55 +00:00
Romain Quetiez
d6da043a32 #722 Document preview disappeared since 2.0 (not in 1.x)
SVN:trunk[2732]
2013-05-14 15:43:22 +00:00
Romain Quetiez
10a7a5aa38 Fix for reconciliation by id (Advanced Mode) -reintegrated from branch 2.0
SVN:trunk[2731]
2013-05-14 15:19:12 +00:00
Romain Quetiez
5684f1e196 #713 URL format reviewed:
- more formats allowed by default
- aligned between the wiki formatting and the URL attribute
- configurable globally with 'url_validation_pattern'
- can be defined at the attribute level with tag validation_pattern


SVN:trunk[2730]
2013-05-14 14:20:00 +00:00
Romain Quetiez
2376a63d18 #711 Audit error drill-down not working when zero error (requires to bookmark the link when there are some errors, then use it later when there is no error anymore)
SVN:trunk[2729]
2013-05-14 10:41:44 +00:00
Romain Quetiez
744b821d03 #710 Query phrase book "fields list" is too small
SVN:trunk[2728]
2013-05-14 10:19:33 +00:00
Romain Quetiez
f3eb6b5cb3 #705 Typo in the english dictionary
SVN:trunk[2727]
2013-05-14 10:09:42 +00:00
Romain Quetiez
d973f64576 #708 Cosmetics for the english dictionary
SVN:trunk[2726]
2013-05-14 10:07:20 +00:00
Denis Flaven
a6c9bcf780 #717: prevent flicker of unstyled content: make the pane visible only when their content is ready... + watchdog in case of error.
SVN:trunk[2725]
2013-05-14 10:03:23 +00:00
Romain Quetiez
7cae338e6d #723 Cron locking is too restrictive
SVN:trunk[2724]
2013-05-14 09:34:08 +00:00
Denis Flaven
ed344650c5 Prevent logoff problems depending on PHP's session.cache_limiter setting...
SVN:trunk[2723]
2013-05-13 15:45:10 +00:00
Romain Quetiez
2d03e95ece Fixed typos in the sample data for the service subcategories
SVN:trunk[2722]
2013-05-13 14:50:56 +00:00
Denis Flaven
69179f5d25 #707: oops, prevent a warning when no dict entry...
SVN:trunk[2721]
2013-05-13 14:50:26 +00:00
Romain Quetiez
27cf82b270 #693 Portal user cannot choose services linked via customer contract
SVN:trunk[2720]
2013-05-13 14:46:06 +00:00
Denis Flaven
d28891eaf4 #707: Make sure that DOCTYPE is the first statement of the page
- Enhancement: support for "target" in PopupMenuItems.. which was ignored before

SVN:trunk[2719]
2013-05-13 14:43:16 +00:00
Erwan Taloc
b1c1e5f9f2 Limit rack according to location for PDU (see track #638)
SVN:trunk[2718]
2013-05-13 10:56:55 +00:00
Erwan Taloc
8a86c6a637 change path for incident icon trac #699
SVN:trunk[2717]
2013-05-13 10:50:00 +00:00
Denis Flaven
631811145f Experimental support of icon uploads...
SVN:trunk[2716]
2013-05-07 17:27:37 +00:00
Denis Flaven
78ff062787 Proper reset of APC cache upon compilation !
SVN:trunk[2715]
2013-05-07 17:25:24 +00:00
Denis Flaven
427f50b390 Allow filtering of the languages to install
SVN:trunk[2713]
2013-05-02 08:09:10 +00:00
Romain Quetiez
21f0d96146 New way to compile the dictionaries, allowing for incremental modification via XML
SVN:trunk[2712]
2013-04-30 16:14:37 +00:00
Romain Quetiez
7fcf922ee0 Updated the unit test for the OQL parser
SVN:trunk[2711]
2013-04-29 09:01:06 +00:00
Denis Flaven
22dc44c9e5 Typo
SVN:trunk[2710]
2013-04-29 08:55:22 +00:00
Denis Flaven
6feb62d728 #383: support negative numbers in OQL
Enhancements: support MySQL bitwise operators (&, |, ^, <<, >>) and hexadecimal numbers (up to 64-bit).

SVN:trunk[2709]
2013-04-29 08:51:01 +00:00
Denis Flaven
29060f7b5e Enhancement: the expression of dependencies between modules can now use a complex boolean expression with a combination of "logical or" (||) and "logical and" (&&) instead of just a module name.
SVN:trunk[2708]
2013-04-29 08:39:23 +00:00
Denis Flaven
6df6af0df0 #663: Fix for emptying a directory which contains broken symbolic links
SVN:trunk[2707]
2013-04-29 08:34:19 +00:00
Romain Quetiez
d433a45200 #692 Compensate the effect of this fix committed as 2705
SVN:trunk[2706]
2013-04-26 10:32:11 +00:00
Romain Quetiez
d5e57ba0ba #692 Allow to set a default value for AttributeDuration (was forced to 0 anytime)
SVN:trunk[2705]
2013-04-26 10:24:44 +00:00
Romain Quetiez
d69236cb25 #701 Portal customization : enabled the calendar widget for input of a date+time type of attribute (it was only available for pure DATE attributes)
SVN:trunk[2704]
2013-04-26 10:07:13 +00:00
Romain Quetiez
422aa5b407 #715 Finalized the fix (added the year for group by day, but it is not displayed)
SVN:trunk[2703]
2013-04-26 09:54:39 +00:00
Romain Quetiez
f97f51b895 #715 Group by day -> month+day, group by month -> year+month, months are shown as a localized label
SVN:trunk[2702]
2013-04-26 09:46:31 +00:00
Denis Flaven
4ea0093f12 Enhancements to the Japanese localization, thanks to Shoji Seki
SVN:trunk[2701]
2013-04-25 09:24:35 +00:00
Romain Quetiez
2e35bb97d0 Compiler: added the capability to redefine icons in xml
SVN:trunk[2700]
2013-04-24 16:02:24 +00:00
Romain Quetiez
519b9f1a73 Needed => define if not exists
SVN:trunk[2699]
2013-04-24 15:50:44 +00:00
Denis Flaven
85c1f0d1aa New internal flag for generating the delta.
SVN:trunk[2698]
2013-04-23 15:33:40 +00:00
Romain Quetiez
f2738a79a0 User Portal:
- Portal power users can see ALL the tickets of the current customer
- Reopen and Close are done in a separate dialog box where the user will input requested data
- On closed tickets, the user satisfaction and comments are displayed

SVN:trunk[2697]
2013-04-18 15:23:43 +00:00
Romain Quetiez
4ae69372d4 User rights:
- for link classes, if no grant has been given explicitely, then check if one of the remote class has a grant (mix of compile-time and run-time changes)
- fixed an issue: when looking for a grant into the hierarchy, the most explicit declaration (aka the deepest class) must be found first -> reorder the parent classes (new flag on MetaModel::EnumParentClasses

SVN:trunk[2696]
2013-04-18 09:03:08 +00:00
Romain Quetiez
cce2509b2e #702 The second implementation of this fix was still incomplete (not compatible with some constraint queries)
SVN:trunk[2694]
2013-04-17 16:11:45 +00:00
Romain Quetiez
aa4d396960 #702 The first implementation of this fiw was not incomplete (not compatible with some constraint queries)
SVN:trunk[2692]
2013-04-17 15:53:34 +00:00
Romain Quetiez
5f11cc4cf8 #703 HTML entities not escaped in history tab
SVN:trunk[2690]
2013-04-16 13:23:10 +00:00
Denis Flaven
956ecda77e Fix for the "Notifications" tab: use the polymorphism to let each trigger determine which object is "In Scope" and thus can potentially have notifications related to it
SVN:trunk[2688]
2013-04-16 12:52:43 +00:00
Erwan Taloc
bb554b1271 define correct file name for en , es_cr, fr dictionnaries
Remove obsolete dictionaries

SVN:trunk[2685]
2013-04-16 11:44:51 +00:00
Erwan Taloc
15000ff48a remove unused dictionaries
SVN:trunk[2684]
2013-04-16 11:38:52 +00:00
Erwan Taloc
14d0ebdd71 define correct file name for en , es_cr, fr dictionnaries
SVN:trunk[2683]
2013-04-16 11:36:52 +00:00
Romain Quetiez
c95b8cf939 #702 Ignoring the class when looking up for a polymorphic ext key
SVN:trunk[2681]
2013-04-16 11:24:34 +00:00
Denis Flaven
74575440d7 Bug fix: properly serialize Emails with binary Attachments. Previously asynchronous emails with binary attachements were not working (they were queued but never sent).
SVN:trunk[2679]
2013-04-16 09:05:56 +00:00
Erwan Taloc
fcb7798e9e Remove dictionaries from module definition
SVN:trunk[2677]
2013-04-11 15:16:10 +00:00
Erwan Taloc
b0a054ada7 Remove dictionaries from module definition
SVN:trunk[2676]
2013-04-11 15:15:09 +00:00
Denis Flaven
b83d42efee Bug fix: allow printing lists fully expanded without the "pager" navigation.
Also try to completely hide the main menu on the left when printing.

SVN:trunk[2675]
2013-04-08 11:57:07 +00:00
Romain Quetiez
2e18c96328 Reviewed the readme file
SVN:trunk[2673]
2013-04-02 14:23:42 +00:00
Denis Flaven
2daccbe29d One more bug fix listed.
SVN:trunk[2672]
2013-04-02 14:13:09 +00:00
Denis Flaven
51d9c30315 #696: the message "Please fill all mandatory fields" is now localized (done in English, French and German)
SVN:trunk[2671]
2013-04-02 13:51:05 +00:00
Romain Quetiez
3d79e3fe8f #472 REST API: finalized the implementation of core/get_related + added the support for AttributeBlob (to manage documents and attachments)
SVN:trunk[2669]
2013-04-02 13:31:52 +00:00
Denis Flaven
57b34d2d91 Preparing for the 2.0.1-beta release...
SVN:trunk[2668]
2013-04-02 10:04:57 +00:00
Denis Flaven
a5a8863b52 Fixed transparent background issues...
SVN:trunk[2667]
2013-04-02 09:48:01 +00:00
Denis Flaven
d92b0aabee First (draft) version of the get_related verb for the REST API.
SVN:trunk[2666]
2013-04-02 09:42:26 +00:00
Denis Flaven
48472b6150 - Added support for creating symbolic links via the toolkit
- Added more debug info in the setup.log about the detection of the previously installed modules

SVN:trunk[2665]
2013-03-29 15:44:06 +00:00
Denis Flaven
fc29424600 #698 SeparatorPopupMenuItem was not working.
SVN:trunk[2664]
2013-03-29 13:53:51 +00:00
Denis Flaven
c4942dd747 #697: properly export NULL dates in "spreadsheet" format.
SVN:trunk[2662]
2013-03-29 13:32:47 +00:00
Denis Flaven
30b9afe165 REST API bug fix: properly handle key->finalclass
SVN:trunk[2661]
2013-03-29 10:54:23 +00:00
Denis Flaven
6d66969ff3 Better display of empty dates in the status report
SVN:trunk[2660]
2013-03-28 10:01:21 +00:00
Denis Flaven
667f258ec2 Preserve POSted parameters on the login web page (useful when the session expires)
SVN:trunk[2659]
2013-03-28 09:59:49 +00:00
Denis Flaven
badff05995 More readable edition for AttributeDuration (number are right justified)
SVN:trunk[2658]
2013-03-28 09:58:28 +00:00
Denis Flaven
052128da81 #694: make $__comp_menus__ really global !
SVN:trunk[2655]
2013-03-25 13:31:51 +00:00
Romain Quetiez
57629051bd #691 Notifications not sent if some recicipients have an empty address
SVN:trunk[2653]
2013-03-21 14:42:34 +00:00
Romain Quetiez
2cc89ad167 #690 XML export broken
SVN:trunk[2651]
2013-03-20 17:26:40 +00:00
Romain Quetiez
387ab05fd2 #674 request_type:servicerequest changed into service_request - added the DB update to allow an upgrade
SVN:trunk[2650]
2013-03-20 15:51:13 +00:00
Romain Quetiez
5de85c9d97 #688 ... regression in the search forms when the autocomplete is active
SVN:trunk[2648]
2013-03-20 15:10:18 +00:00
Romain Quetiez
d7fa2ca5b9 #688 When the autocomplete is activated, and the allowed values depend on another value, then it is possible to set a wrong value
SVN:trunk[2646]
2013-03-20 14:47:52 +00:00
Romain Quetiez
3d2866a2a0 #687 Finalized the bug fixed started in [2632], about attributes/hierarchies/labels (dictionary)
SVN:trunk[2644]
2013-03-20 10:00:19 +00:00
Denis Flaven
5d805a123e #683 : allow installation on a DB which names begins with numbers
+ directory cleanup for supporting symblinks

SVN:trunk[2639]
2013-03-19 11:38:53 +00:00
Erwan Taloc
584e1fade0 Add reconciliation keys for SLT in order to allow import for SLT having the same name
SVN:trunk[2638]
2013-03-18 17:49:27 +00:00
Erwan Taloc
98fa1fd071 Add reconciliation keys for SLT in order to allow import for SLT having the same name
SVN:trunk[2637]
2013-03-18 17:48:43 +00:00
Erwan Taloc
2b0b34a3a6 Remove wrong dependency to service_id on parent_request_id
SVN:trunk[2636]
2013-03-18 17:47:31 +00:00
Erwan Taloc
a3213bba43 Remove wrong dependency to service_id on parent_request_id
SVN:trunk[2635]
2013-03-18 17:46:56 +00:00
Erwan Taloc
1fc6b945f6 Add reconciliation key for Software (Fix Trac #666)
SVN:trunk[2634]
2013-03-18 17:46:02 +00:00
Denis Flaven
58aaed567f Bug fix: Data model alternate options were not properly checked when upgrading (especially when selecting ITIL tickets)
SVN:trunk[2633]
2013-03-18 17:39:26 +00:00
Romain Quetiez
51628604bf #687 Label for attribute Person:name always shown in english (Last Name)
SVN:trunk[2632]
2013-03-18 17:38:28 +00:00
Romain Quetiez
a35ff29363 #677 Cosmetics in the german localization (and a few other languages): first header of the config mgmt overview
SVN:trunk[2631]
2013-03-18 17:00:14 +00:00
Romain Quetiez
b52be60776 #684 CSV import / reconciliation using an enum does not take the translation into account
SVN:trunk[2630]
2013-03-18 10:26:14 +00:00
Romain Quetiez
d60d634208 #682 Order notifications (last first).
SVN:trunk[2629]
2013-03-15 16:25:02 +00:00
Romain Quetiez
b112597df8 Compiler: typo preventing from setting the property 'min_autocomplete_chars' on an external key
SVN:trunk[2628]
2013-03-15 10:07:50 +00:00
Romain Quetiez
a965bbd39f #626 Fixed missing translation in dictionaries (Tickets: "relations", and Contacts overview / count)
SVN:trunk[2627]
2013-03-14 17:37:19 +00:00
Romain Quetiez
cc70570e65 #657 JavaScript error when modifying UserLDAP object with Sync
SVN:trunk[2626]
2013-03-14 14:46:51 +00:00
Romain Quetiez
c1fae7fd24 #659 exception handling producing notices, hence causing confusion
SVN:trunk[2625]
2013-03-14 14:22:27 +00:00
Romain Quetiez
d620516055 #675 Error when drilling down on graph/pie/table with group by on a field that can be null (this case has been excluded)
SVN:trunk[2624]
2013-03-14 13:00:28 +00:00
Romain Quetiez
0918c81d58 #680 Setup failing to display the check report when DOM extension not enabled (php-xml not installed on redhat distributions)
SVN:trunk[2623]
2013-03-14 10:14:53 +00:00
Romain Quetiez
39b79e2a05 #660 Warning raised with ZendServer (with APC cache enabled) causing the setup to fail
SVN:trunk[2622]
2013-03-14 10:12:05 +00:00
Romain Quetiez
3340ca2b10 #679 Improved the reporting in case of an error while loading a module: 1) the list of already loaded modules is given, 2) the full path of the searched node is given
SVN:trunk[2621]
2013-03-14 09:07:35 +00:00
Denis Flaven
67dc148069 Better error reporting when loading a module fails.
SVN:trunk[2620]
2013-03-13 16:54:32 +00:00
Denis Flaven
c6ba656f1d Sort the modules before processing them for dependencies, in order to obtain a predictable result independent from the order of the modules in the file system... hopefully... (should fix Trac#679)
SVN:trunk[2619]
2013-03-13 16:52:10 +00:00
Romain Quetiez
acf4c7a28a #664 Could not login after upgrade of an iTop 1.x with a DB prefix
SVN:trunk[2618]
2013-03-13 14:37:46 +00:00
Romain Quetiez
b38dea4bba #634 Detection of HTTPS not working with nginx (iTop always considering the current connection as being secure)
SVN:trunk[2617]
2013-03-13 13:57:51 +00:00
Romain Quetiez
3cf398618e #472 REST Services: added core/delete (to bulk delete, full-featured), and validated the operation core/apply_stimulus
SVN:trunk[2616]
2013-03-13 11:01:16 +00:00
Romain Quetiez
462f163d8a #661 and #662 Could not create a user request (or ?) as soon as the autocomplete feature gets active
SVN:trunk[2615]
2013-03-12 16:09:03 +00:00
Erwan Taloc
96b3e9a891 Fix issue for creation of Logical Interface
SVN:trunk[2614]
2013-03-10 09:08:04 +00:00
Erwan Taloc
b56242808d Fix bug TTO / TTR computation for Service request (Trac 674)
SVN:trunk[2613]
2013-03-10 08:50:42 +00:00
Erwan Taloc
793f94d302 Correction to display IP field for Physical Device :
Network Device
Server
Storage System
NAS
Tape Library
SAN switch

SVN:trunk[2612]
2013-03-10 08:46:57 +00:00
Romain Quetiez
a23569e0c4 Fixed issue with the new CRON execution pattern
SVN:trunk[2611]
2013-03-08 14:03:55 +00:00
Romain Quetiez
80a8b63498 Modified the mechanism to display object dedicated messages (allows the plugin to add their message or replace standard ones)
Factorized the code to bulk update / bulk delete objects in an interactive way.

SVN:trunk[2610]
2013-03-08 13:36:31 +00:00
Denis Flaven
eee8d71381 Enhancement: take into account the "periodicity" of the background processes.
Bug fix: fixed a warning in CheckStopWatchThresholds

SVN:trunk[2609]
2013-03-01 17:14:54 +00:00
Denis Flaven
9013910cec Protect against unwanted output that may corrupt the JSON results
SVN:trunk[2607]
2013-02-27 16:14:16 +00:00
Denis Flaven
ddff9180ac Added 'core/apply_stimulus' as a possible operation for the REST web services.
SVN:trunk[2605]
2013-02-27 15:58:16 +00:00
Denis Flaven
4988d6eb04 Allow retrieving of the Filter used by a DisplayBlock (useful for extending search forms)
SVN:trunk[2603]
2013-02-27 14:31:27 +00:00
Denis Flaven
5ff86a40d9 Fix for making iUIPageExtension usable !
SVN:trunk[2600]
2013-02-27 13:37:12 +00:00
Denis Flaven
05133aa319 Fix for supporting the CSV export of big audit results.
SVN:trunk[2598]
2013-02-06 15:44:59 +00:00
Denis Flaven
626e2a1db1 Enable support of databases which name either is a reserved word or contains non-alphanumeric characters (i.e. itop-production).
SVN:trunk[2595]
2013-01-31 15:14:23 +00:00
Romain Quetiez
59e1a64f2d REST services: fixed issue with returned external keys
SVN:trunk[2594]
2013-01-30 14:09:32 +00:00
Denis Flaven
a9ac7d9e10 Added support for GET/JSON-P
SVN:trunk[2593]
2013-01-30 13:49:12 +00:00
Romain Quetiez
7b2789479d REST services: alpha2. It is now extensible (implement iRestServiceProvider). Still lacks two verbs: apply_stimulus and delete.
SVN:trunk[2592]
2013-01-30 09:46:55 +00:00
Denis Flaven
4e8db37060 Added credits to Stephan Rickauer about security.
SVN:trunk[2591]
2013-01-22 17:55:20 +00:00
Denis Flaven
80b0a8b942 Fix for Trac #670: XSS vulnerability issue.
SVN:trunk[2589]
2013-01-22 17:39:16 +00:00
Romain Quetiez
32924bc054 REST services: an alpha version. It is already possible to create/update/get objects. An example illustrates the possibilities.
SVN:trunk[2586]
2013-01-16 13:57:51 +00:00
Romain Quetiez
96530a5bdf Auto-documentation: first step => the extension APIs
SVN:trunk[2585]
2013-01-04 13:43:52 +00:00
Denis Flaven
18eee44ee6 Bug fix: properly record history of Hierarchical Keys
SVN:trunk[2584]
2012-12-21 15:37:45 +00:00
561 changed files with 57865 additions and 44594 deletions

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -210,6 +210,25 @@ class URP_Profiles extends UserRightsBaseClassGUI
$oLnk->DBDelete();
}
}
/**
* Returns the set of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...)
* for the given attribute in the current state of the object
* @param $sAttCode string $sAttCode The code of the attribute
* @param $aReasons array To store the reasons why the attribute is read-only (info about the synchro replicas)
* @param $sTargetState string The target state in which to evalutate the flags, if empty the current state will be used
* @return integer Flags: the binary combination of the flags applicable to this attribute
*/
public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '')
{
$iFlags = parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState);
if (MetaModel::GetConfig()->Get('demo_mode'))
{
$aReasons[] = 'Sorry, profiles are read-only in the demonstration mode!';
$iFlags |= OPT_ATT_READONLY;
}
return $iFlags;
}
}
@@ -563,72 +582,10 @@ exit;
return true;
}
$oExpression = new FieldExpression($sAttCode, $sClass);
$oFilter = new DBObjectSearch($sClass);
$oListExpr = ListExpression::FromScalars($aUserOrgs);
$oCondition = new BinaryExpression($oExpression, 'IN', $oListExpr);
$oFilter->AddConditionExpression($oCondition);
if (self::HasSharing())
{
if (($sAttCode == 'id') && isset($aSettings['bSearchMode']) && $aSettings['bSearchMode'])
{
// Querying organizations (or derived)
// and the expected list of organizations will be used as a search criteria
// Therefore the query can also return organization having objects shared with the allowed organizations
//
// 1) build the list of organizations sharing something with the allowed organizations
// Organization <== sharing_org_id == SharedObject having org_id IN {user orgs}
$oShareSearch = new DBObjectSearch('SharedObject');
$oOrgField = new FieldExpression('org_id', 'SharedObject');
$oShareSearch->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr));
$oSearchSharers = new DBObjectSearch('Organization');
$oSearchSharers->AllowAllData();
$oSearchSharers->AddCondition_ReferencedBy($oShareSearch, 'sharing_org_id');
$aSharers = array();
foreach($oSearchSharers->ToDataArray(array('id')) as $aRow)
{
$aSharers[] = $aRow['id'];
}
// 2) Enlarge the overall results: ... OR id IN(id1, id2, id3)
if (count($aSharers) > 0)
{
$oSharersList = ListExpression::FromScalars($aSharers);
$oFilter->MergeConditionExpression(new BinaryExpression($oExpression, 'IN', $oSharersList));
}
}
$aShareProperties = SharedObject::GetSharedClassProperties($sClass);
if ($aShareProperties)
{
$sShareClass = $aShareProperties['share_class'];
$sShareAttCode = $aShareProperties['attcode'];
$oSearchShares = new DBObjectSearch($sShareClass);
$oSearchShares->AllowAllData();
$sHierarchicalKeyCode = MetaModel::IsHierarchicalClass('Organization');
$oOrgField = new FieldExpression('org_id', $sShareClass);
$oSearchShares->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr));
$aShared = array();
foreach($oSearchShares->ToDataArray(array($sShareAttCode)) as $aRow)
{
$aShared[] = $aRow[$sShareAttCode];
}
if (count($aShared) > 0)
{
$oObjId = new FieldExpression('id', $sClass);
$oSharedIdList = ListExpression::FromScalars($aShared);
$oFilter->MergeConditionExpression(new BinaryExpression($oObjId, 'IN', $oSharedIdList));
}
}
} // if HasSharing
return $oFilter;
return $this->MakeSelectFilter($sClass, $aUserOrgs, $aSettings, $sAttCode);
}
// This verb has been made public to allow the development of an accurate feedback for the current configuration
public function GetProfileActionGrant($iProfile, $sClass, $sAction)
{

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -822,70 +822,7 @@ exit;
return true;
}
$oExpression = new FieldExpression($sAttCode, $sClass);
$oFilter = new DBObjectSearch($sClass);
$oListExpr = ListExpression::FromScalars($aUserOrgs);
$oCondition = new BinaryExpression($oExpression, 'IN', $oListExpr);
$oFilter->AddConditionExpression($oCondition);
if (self::HasSharing())
{
if (($sAttCode == 'id') && isset($aSettings['bSearchMode']) && $aSettings['bSearchMode'])
{
// Querying organizations (or derived)
// and the expected list of organizations will be used as a search criteria
// Therefore the query can also return organization having objects shared with the allowed organizations
//
// 1) build the list of organizations sharing something with the allowed organizations
// Organization <== sharing_org_id == SharedObject having org_id IN {user orgs}
$oShareSearch = new DBObjectSearch('SharedObject');
$oOrgField = new FieldExpression('org_id', 'SharedObject');
$oShareSearch->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr));
$oSearchSharers = new DBObjectSearch('Organization');
$oSearchSharers->AllowAllData();
$oSearchSharers->AddCondition_ReferencedBy($oShareSearch, 'sharing_org_id');
$aSharers = array();
foreach($oSearchSharers->ToDataArray(array('id')) as $aRow)
{
$aSharers[] = $aRow['id'];
}
// 2) Enlarge the overall results: ... OR id IN(id1, id2, id3)
if (count($aSharers) > 0)
{
$oSharersList = ListExpression::FromScalars($aSharers);
$oFilter->MergeConditionExpression(new BinaryExpression($oExpression, 'IN', $oSharersList));
}
}
$aShareProperties = SharedObject::GetSharedClassProperties($sClass);
if ($aShareProperties)
{
$sShareClass = $aShareProperties['share_class'];
$sShareAttCode = $aShareProperties['attcode'];
$oSearchShares = new DBObjectSearch($sShareClass);
$oSearchShares->AllowAllData();
$sHierarchicalKeyCode = MetaModel::IsHierarchicalClass('Organization');
$oOrgField = new FieldExpression('org_id', $sShareClass);
$oSearchShares->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr));
$aShared = array();
foreach($oSearchShares->ToDataArray(array($sShareAttCode)) as $aRow)
{
$aShared[] = $aRow[$sShareAttCode];
}
if (count($aShared) > 0)
{
$oObjId = new FieldExpression('id', $sClass);
$oSharedIdList = ListExpression::FromScalars($aShared);
$oFilter->MergeConditionExpression(new BinaryExpression($oObjId, 'IN', $oSharedIdList));
}
}
} // if HasSharing
return $oFilter;
return $this->MakeSelectFilter($sClass, $aUserOrgs, $aSettings, $sAttCode);
}
// This verb has been made public to allow the development of an accurate feedback for the current configuration

View File

@@ -26,16 +26,14 @@
require_once(APPROOT."/application/webpage.class.inc.php");
class ajax_page extends WebPage
class ajax_page extends WebPage implements iTabbedPage
{
/**
* Jquery style ready script
* @var Hash
*/
protected $m_sReadyScript;
protected $m_sCurrentTab;
protected $m_sCurrentTabContainer;
protected $m_aTabs;
protected $m_oTabs;
private $m_sMenu; // If set, then the menu will be updated
/**
@@ -48,9 +46,7 @@ class ajax_page extends WebPage
$this->m_sReadyScript = "";
//$this->add_header("Content-type: text/html; charset=utf-8");
$this->add_header("Cache-control: no-cache");
$this->m_sCurrentTabContainer = '';
$this->m_sCurrentTab = '';
$this->m_aTabs = array();
$this->m_oTabs = new TabManager();
$this->sContentType = 'text/html';
$this->sContentDisposition = 'inline';
$this->m_sMenu = "";
@@ -58,41 +54,69 @@ class ajax_page extends WebPage
public function AddTabContainer($sTabContainer, $sPrefix = '')
{
$this->m_aTabs[$sTabContainer] = array('content' =>'', 'prefix' => $sPrefix);
$this->add("\$Tabs:$sTabContainer\$");
$this->add($this->m_oTabs->AddTabContainer($sTabContainer, $sPrefix));
}
public function AddToTab($sTabContainer, $sTabLabel, $sHtml)
{
if (!isset($this->m_aTabs[$sTabContainer]['content'][$sTabLabel]))
{
// Set the content of the tab
$this->m_aTabs[$sTabContainer]['content'][$sTabLabel] = $sHtml;
}
else
{
// Append to the content of the tab
$this->m_aTabs[$sTabContainer]['content'][$sTabLabel] .= $sHtml;
}
$this->add($this->m_oTabs->AddToTab($sTabContainer, $sTabLabel, $sHtml));
}
public function SetCurrentTabContainer($sTabContainer = '')
{
$sPreviousTabContainer = $this->m_sCurrentTabContainer;
$this->m_sCurrentTabContainer = $sTabContainer;
return $sPreviousTabContainer;
return $this->m_oTabs->SetCurrentTabContainer($sTabContainer);
}
public function SetCurrentTab($sTabLabel = '')
{
$sPreviousTab = $this->m_sCurrentTab;
$this->m_sCurrentTab = $sTabLabel;
return $sPreviousTab;
return $this->m_oTabs->SetCurrentTab($sTabLabel);
}
/**
* Add a tab which content will be loaded asynchronously via the supplied URL
*
* Limitations:
* Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to pull content from another server.
* Static content cannot be added inside such tabs.
*
* @param string $sTabLabel The (localised) label of the tab
* @param string $sUrl The URL to load (on the same server)
* @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause the tab to be reloaded upon each activation.
* @since 2.0.3
*/
public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true)
{
$this->add($this->m_oTabs->AddAjaxTab($sTabLabel, $sUrl, $bCache));
}
public function GetCurrentTab()
{
return $this->m_sCurrentTab;
return $this->m_oTabs->GetCurrentTab();
}
public function RemoveTab($sTabLabel, $sTabContainer = null)
{
$this->m_oTabs->RemoveTab($sTabLabel, $sTabContainer);
}
/**
* Finds the tab whose title matches a given pattern
* @return mixed The name of the tab as a string or false if not found
*/
public function FindTab($sPattern, $sTabContainer = null)
{
return $this->m_oTabs->FindTab($sPattern, $sTabContainer);
}
/**
* Make the given tab the active one, as if it were clicked
* DOES NOT WORK: apparently in the *old* version of jquery
* that we are using this is not supported... TO DO upgrade
* the whole jquery bundle...
*/
public function SelectTab($sTabContainer, $sTabLabel)
{
$this->add_ready_script($this->m_oTabs->SelectTab($sTabContainer, $sTabLabel));
}
public function AddToMenu($sHtml)
@@ -118,13 +142,26 @@ class ajax_page extends WebPage
{
header($s_header);
}
if (count($this->m_aTabs) > 0)
if ($this->m_oTabs->TabsContainerCount() > 0)
{
$this->add_ready_script(
<<<EOF
// The "tab widgets" to handle.
var tabs = $('div[id^=tabbedContent]');
// Ugly patch for a change in the behavior of jQuery UI:
// Before jQuery UI 1.9, tabs were always considered as "local" (opposed to Ajax)
// when their href was beginning by #. Starting with 1.9, a <base> tag in the page
// is taken into account and causes "local" tabs to be considered as Ajax
// unless their URL is equal to the URL of the page...
if ($('base').length > 0)
{
$('div[id^=tabbedContent] > ul > li > a').each(function() {
var sHash = location.hash;
var sCleanLocation = location.href.toString().replace(sHash, '').replace(/#$/, '');
$(this).attr("href", sCleanLocation+$(this).attr("href"));
});
}
if ($.bbq)
{
// This selector will be reused when selecting actual tab widget A elements.
@@ -160,36 +197,7 @@ EOF
);
}
// Render the tabs in the page (if any)
foreach($this->m_aTabs as $sTabContainerName => $aTabContainer)
{
$sTabs = '';
$m_aTabs = $aTabContainer['content'];
$sPrefix = $aTabContainer['prefix'];
$container_index = 0;
if (count($m_aTabs) > 0)
{
$sTabs = "<!-- tabs -->\n<div id=\"tabbedContent_{$sPrefix}{$sTabContainerName}\" class=\"light\">\n";
$sTabs .= "<ul>\n";
// Display the unordered list that will be rendered as the tabs
$i = 0;
foreach($m_aTabs as $sTabName => $sTabContent)
{
$sTabs .= "<li><a href=\"#tab_{$sPrefix}$i\" class=\"tab\"><span>".htmlentities($sTabName, ENT_QUOTES, 'UTF-8')."</span></a></li>\n";
$i++;
}
$sTabs .= "</ul>\n";
// Now add the content of the tabs themselves
$i = 0;
foreach($m_aTabs as $sTabName => $sTabContent)
{
$sTabs .= "<div id=\"tab_{$sPrefix}$i\">".$sTabContent."</div>\n";
$i++;
}
$sTabs .= "</div>\n<!-- end of tabs-->\n";
}
$this->s_content = str_replace("\$Tabs:$sTabContainerName\$", $sTabs, $this->s_content);
$container_index++;
}
$this->s_content = $this->m_oTabs->RenderIntoContent($this->s_content);
// Additional UI widgets to be activated inside the ajax fragment ??
if (($this->sContentType == 'text/html') && (preg_match('/class="date-pick"/', $this->s_content) || preg_match('/class="datetime-pick"/', $this->s_content)) )
@@ -289,9 +297,9 @@ EOF
public function add($sHtml)
{
if (!empty($this->m_sCurrentTabContainer) && !empty($this->m_sCurrentTab))
if (($this->m_oTabs->GetCurrentTabContainer() != '') && ($this->m_oTabs->GetCurrentTab() != ''))
{
$this->AddToTab($this->m_sCurrentTabContainer, $this->m_sCurrentTab, $sHtml);
$this->m_oTabs->AddToTab($this->m_oTabs->GetCurrentTabContainer(), $this->m_oTabs->GetCurrentTab(), $sHtml);
}
else
{
@@ -305,15 +313,18 @@ EOF
*/
public function start_capture()
{
if (!empty($this->m_sCurrentTabContainer) && !empty($this->m_sCurrentTab))
{
$iOffset = isset($this->m_aTabs[$this->m_sCurrentTabContainer]['content'][$this->m_sCurrentTab]) ? strlen($this->m_aTabs[$this->m_sCurrentTabContainer]['content'][$this->m_sCurrentTab]): 0;
return array('tc' => $this->m_sCurrentTabContainer, 'tab' => $this->m_sCurrentTab, 'offset' => $iOffset);
}
else
{
return parent::start_capture();
}
$sCurrentTabContainer = $this->m_oTabs->GetCurrentTabContainer();
$sCurrentTab = $this->m_oTabs->GetCurrentTab();
if (!empty($sCurrentTabContainer) && !empty($sCurrentTab))
{
$iOffset = $this->m_oTabs->GetCurrentTabLength();
return array('tc' => $sCurrentTabContainer, 'tab' => $sCurrentTab, 'offset' => $iOffset);
}
else
{
return parent::start_capture();
}
}
/**
@@ -324,23 +335,22 @@ EOF
*/
public function end_capture($offset)
{
if (is_array($offset))
{
if (isset($this->m_aTabs[$offset['tc']]['content'][$offset['tab']]))
{
$sCaptured = substr($this->m_aTabs[$offset['tc']]['content'][$offset['tab']], $offset['offset']);
$this->m_aTabs[$offset['tc']]['content'][$offset['tab']] = substr($this->m_aTabs[$offset['tc']]['content'][$offset['tab']], 0, $offset['offset']);
}
else
{
$sCaptured = '';
}
}
else
{
$sCaptured = parent::end_capture($offset);
}
return $sCaptured;
if (is_array($offset))
{
if ($this->m_oTabs->TabExists($offset['tc'], $offset['tab']))
{
$sCaptured = $this->m_oTabs->TruncateTab($offset['tc'], $offset['tab'], $offset['offset']);
}
else
{
$sCaptured = '';
}
}
else
{
$sCaptured = parent::end_capture($offset);
}
return $sCaptured;
}
/**

View File

@@ -31,6 +31,7 @@ require_once(APPROOT.'/application/sqlblock.class.inc.php');
require_once(APPROOT.'/application/audit.category.class.inc.php');
require_once(APPROOT.'/application/audit.rule.class.inc.php');
require_once(APPROOT.'/application/query.class.inc.php');
require_once(APPROOT.'/setup/moduleinstallation.class.inc.php');
//require_once(APPROOT.'/application/menunode.class.inc.php');
require_once(APPROOT.'/application/utils.inc.php');

View File

@@ -17,34 +17,248 @@
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* Class iPlugin
* Management of application plugin
* Management of application plugins
*
* Definition of interfaces that can be implemented to customize iTop.
* You may implement such interfaces in a module file (e.g. main.mymodule.php)
*
* @package Extensibility
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
* @api
*/
/**
* Implement this interface to change the behavior of the GUI for some objects.
*
* All methods are invoked by iTop for a given object. There are basically two usages:
*
* 1) To tweak the form of an object, you will have to implement a specific behavior within:
*
* * OnDisplayProperties (bEditMode = true)
* * OnFormSubmit
* * OnFormCancel
*
* 2) To tune the display of the object details, you can use:
*
* * OnDisplayProperties
* * OnDisplayRelations
* * GetIcon
* * GetHilightClass
*
* Please note that some of the APIs can be called several times for a single page displayed.
* Therefore it is not recommended to perform too many operations, such as querying the database.
* A recommended pattern is to cache data by the mean of static members.
*
* @package Extensibility
* @api
*/
interface iApplicationUIExtension
{
/**
* Invoked when an object is being displayed (wiew or edit)
*
* The method is called right after the main tab has been displayed.
* You can add output to the page, either to change the display, or to add a form input
*
* Example:
* <code>
* if ($bEditMode)
* {
* $oPage->p('Age of the captain: &lt;input type="text" name="captain_age"/&gt;');
* }
* else
* {
* $oPage->p('Age of the captain: '.$iCaptainAge);
* }
* </code>
*
* @param DBObject $oObject The object being displayed
* @param WebPage $oPage The output context
* @param boolean $bEditMode True if the edition form is being displayed
* @return void
*/
public function OnDisplayProperties($oObject, WebPage $oPage, $bEditMode = false);
public function OnDisplayRelations($oObject, WebPage $oPage, $bEditMode = false);
public function OnFormSubmit($oObject, $sFormPrefix = '');
public function OnFormCancel($sTempId); // temp id is made of session_id and transaction_id, it identifies the object in a unique way
/**
* Invoked when an object is being displayed (wiew or edit)
*
* The method is called rigth after all the tabs have been displayed
*
* @param DBObject $oObject The object being displayed
* @param WebPage $oPage The output context
* @param boolean $bEditMode True if the edition form is being displayed
* @return void
*/
public function OnDisplayRelations($oObject, WebPage $oPage, $bEditMode = false);
/**
* Invoked when the end-user clicks on Modify from the object edition form
*
* The method is called after the changes from the standard form have been
* taken into account, and before saving the changes into the database.
*
* @param DBObject $oObject The object being edited
* @param string $sFormPrefix Prefix given to the HTML form inputs
* @return void
*/
public function OnFormSubmit($oObject, $sFormPrefix = '');
/**
* Invoked when the end-user clicks on Cancel from the object edition form
*
* Implement here any cleanup. This is necessary when you have injected some
* javascript into the edition form, and if that code requires to store temporary data
* (this is the case when a file must be uploaded).
*
* @param string $sTempId Unique temporary identifier made of session_id and transaction_id. It identifies the object in a unique way.
* @return void
*/
public function OnFormCancel($sTempId);
/**
* Not yet called by the framework!
*
* Sorry, the verb has been reserved. You must implement it, but it is not called as of now.
*
* @param DBObject $oObject The object being displayed
* @return type desc
*/
public function EnumUsedAttributes($oObject); // Not yet implemented
/**
* Not yet called by the framework!
*
* Sorry, the verb has been reserved. You must implement it, but it is not called as of now.
*
* @param DBObject $oObject The object being displayed
* @return string Path of the icon, relative to the modules directory.
*/
public function GetIcon($oObject); // Not yet implemented
/**
* Invoked when the object is displayed alone or within a list
*
* Returns a value influencing the appearance of the object depending on its
* state.
*
* Possible values are:
*
* * HILIGHT_CLASS_CRITICAL
* * HILIGHT_CLASS_WARNING
* * HILIGHT_CLASS_OK
* * HILIGHT_CLASS_NONE
*
* @param DBObject $oObject The object being displayed
* @return integer The value representing the mood of the object
*/
public function GetHilightClass($oObject);
/**
* Called when building the Actions menu for a single object or a list of objects
*
* Use this to add items to the Actions menu. You will have to specify a label and an URL.
*
* Example:
* <code>
* $oObject = $oSet->fetch();
* if ($oObject instanceof Sheep)
* {
* return array('View in my app' => 'http://myserver/view_sheeps?id='.$oObject->Get('name'));
* }
* else
* {
* return array();
* }
* </code>
*
* See also iPopupMenuExtension for greater flexibility
*
* @param DBObjectSet $oSet A set of persistent objects (DBObject)
* @return string[string]
*/
public function EnumAllowedActions(DBObjectSet $oSet);
}
/**
* Implement this interface to perform specific things when objects are manipulated
*
* Note that those methods will be called when objects are manipulated, either in a programmatic way
* or through the GUI.
*
* @package Extensibility
* @api
*/
interface iApplicationObjectExtension
{
/**
* Invoked to determine wether an object has been modified in memory
*
* The GUI calls this verb to determine the message that will be displayed to the end-user.
* Anyhow, this API can be called in other contexts such as the CSV import tool.
*
* If the extension returns false, then the framework will perform the usual evaluation.
* Otherwise, the answer is definitively "yes, the object has changed".
*
* @param DBObject $oObject The target object
* @return boolean True if something has changed for the target object
*/
public function OnIsModified($oObject);
/**
* Invoked to determine wether an object can be written to the database
*
* The GUI calls this verb and reports any issue.
* Anyhow, this API can be called in other contexts such as the CSV import tool.
*
* @param DBObject $oObject The target object
* @return string[] A list of errors message. An error message is made of one line and it can be displayed to the end-user.
*/
public function OnCheckToWrite($oObject);
/**
* Invoked to determine wether an object can be deleted from the database
*
* The GUI calls this verb and stops the deletion process if any issue is reported.
*
* Please not that it is not possible to cascade deletion by this mean: only stopper issues can be handled.
*
* @param DBObject $oObject The target object
* @return string[] A list of errors message. An error message is made of one line and it can be displayed to the end-user.
*/
public function OnCheckToDelete($oObject);
/**
* Invoked when an object is updated into the database
*
* The method is called right <b>after</b> the object has been written to the database.
*
* @param DBObject $oObject The target object
* @param CMDBChange|null $oChange A change context. Since 2.0 it is fine to ignore it, as the framework does maintain this information once for all the changes made within the current page
* @return void
*/
public function OnDBUpdate($oObject, $oChange = null);
/**
* Invoked when an object is created into the database
*
* The method is called right <b>after</b> the object has been written to the database.
*
* @param DBObject $oObject The target object
* @param CMDBChange|null $oChange A change context. Since 2.0 it is fine to ignore it, as the framework does maintain this information once for all the changes made within the current page
* @return void
*/
public function OnDBInsert($oObject, $oChange = null);
/**
* Invoked when an object is deleted from the database
*
* The method is called right <b>before</b> the object will be deleted from the database.
*
* @param DBObject $oObject The target object
* @param CMDBChange|null $oChange A change context. Since 2.0 it is fine to ignore it, as the framework does maintain this information once for all the changes made within the current page
* @return void
*/
public function OnDBDelete($oObject, $oChange = null);
}
@@ -54,46 +268,102 @@ interface iApplicationObjectExtension
*
* To add some menus into iTop, declare a class that implements this interface, it will be called automatically
* by the application, as long as the class definition is included somewhere in the code
*
* @package Extensibility
* @api
* @since 2.0
*/
interface iPopupMenuExtension
{
// Possible types of menu into which new items can be added
const MENU_OBJLIST_ACTIONS = 1; // $param is a DBObjectSet containing the list of objects
const MENU_OBJLIST_TOOLKIT = 2; // $param is a DBObjectSet containing the list of objects
const MENU_OBJDETAILS_ACTIONS = 3; // $param is a DBObject instance: the object currently displayed
const MENU_DASHBOARD_ACTIONS = 4; // $param is a Dashboard instance: the dashboard currently displayed
const MENU_USER_ACTIONS = 5; // $param is a null ??
/**
* Insert an item into the Actions menu of a list
*
* $param is a DBObjectSet containing the list of objects
*/
const MENU_OBJLIST_ACTIONS = 1;
/**
* Insert an item into the Toolkit menu of a list
*
* $param is a DBObjectSet containing the list of objects
*/
const MENU_OBJLIST_TOOLKIT = 2;
/**
* Insert an item into the Actions menu on an object details page
*
* $param is a DBObject instance: the object currently displayed
*/
const MENU_OBJDETAILS_ACTIONS = 3;
/**
* Insert an item into the Dashboard menu
*
* The dashboad menu is shown on the top right corner when a dashboard
* is being displayed.
*
* $param is a Dashboard instance: the dashboard currently displayed
*/
const MENU_DASHBOARD_ACTIONS = 4;
/**
* Insert an item into the User menu (upper right corner)
*
* $param is null
*/
const MENU_USER_ACTIONS = 5;
/**
* Get the list of items to be added to a menu. The items will be inserted in the menu in the order of the returned array
* @param int $iMenuId The identifier of the type of menu, as listed by the constants MENU_xxx above
* Get the list of items to be added to a menu.
*
* This method is called by the framework for each menu.
* The items will be inserted in the menu in the order of the returned array.
* @param int $iMenuId The identifier of the type of menu, as listed by the constants MENU_xxx
* @param mixed $param Depends on $iMenuId, see the constants defined above
* @return Array An array of ApplicationPopupMenuItem or an empty array if no action is to be added to the menu
* @return object[] An array of ApplicationPopupMenuItem or an empty array if no action is to be added to the menu
*/
public static function EnumItems($iMenuId, $param);
}
/**
* Each menu items is defined by an instance of an object derived from the class
* ApplicationPopupMenu below
*
* Base class for the various types of custom menus
*
* @package Extensibility
* @internal
* @since 2.0
*/
abstract class ApplicationPopupMenuItem
{
/** @ignore */
protected $sUID;
/** @ignore */
protected $sLabel;
/**
* Constructor
*
* @param string $sUID The unique identifier of this menu in iTop... make sure you pass something unique enough
* @param string $sLabel The display label of the menu (must be localized)
*/
public function __construct($sUID, $sLabel)
{
$this->sUID = $sUID;
$this->sLabel = $sLabel;
}
/**
* Get the UID
*
* @return string The unique identifier
* @ignore
*/
public function GetUID()
{
return $this->sUID;
}
/**
* Get the label
*
* @return string The label
* @ignore
*/
public function GetLabel()
{
return $this->sLabel;
@@ -102,9 +372,11 @@ abstract class ApplicationPopupMenuItem
/**
* Returns the components to create a popup menu item in HTML
* @return Hash A hash array: array('label' => , 'url' => , 'target' => , 'onclick' => )
* @ignore
*/
abstract public function GetMenuItem();
/** @ignore */
public function GetLinkedScripts()
{
return array();
@@ -113,14 +385,21 @@ abstract class ApplicationPopupMenuItem
/**
* Class for adding an item into a popup menu that browses to the given URL
*
* @package Extensibility
* @api
* @since 2.0
*/
class URLPopupMenuItem extends ApplicationPopupMenuItem
{
/** @ignore */
protected $sURL;
/** @ignore */
protected $sTarget;
/**
* Class for adding an item that browses to the given URL
* Constructor
*
* @param string $sUID The unique identifier of this menu in iTop... make sure you pass something unique enough
* @param string $sLabel The display label of the menu (must be localized)
* @param string $sURL If the menu is an hyperlink, provide the absolute hyperlink here
@@ -133,6 +412,7 @@ class URLPopupMenuItem extends ApplicationPopupMenuItem
$this->sTarget = $sTarget;
}
/** @ignore */
public function GetMenuItem()
{
return array ('label' => $this->GetLabel(), 'url' => $this->sURL, 'target' => $this->sTarget);
@@ -141,10 +421,16 @@ class URLPopupMenuItem extends ApplicationPopupMenuItem
/**
* Class for adding an item into a popup menu that triggers some Javascript code
*
* @package Extensibility
* @api
* @since 2.0
*/
class JSPopupMenuItem extends ApplicationPopupMenuItem
{
/** @ignore */
protected $sJSCode;
/** @ignore */
protected $aIncludeJSFiles;
/**
@@ -161,11 +447,14 @@ class JSPopupMenuItem extends ApplicationPopupMenuItem
$this->aIncludeJSFiles = $aIncludeJSFiles;
}
/** @ignore */
public function GetMenuItem()
{
return array ('label' => $this->GetLabel(), 'onclick' => $this->sJSCode, 'url' => '#');
// Note: the semicolumn is a must here!
return array ('label' => $this->GetLabel(), 'onclick' => $this->sJSCode.'; return false;', 'url' => '#');
}
/** @ignore */
public function GetLinkedScripts()
{
return $this->aIncludeJSFiles;
@@ -175,17 +464,23 @@ class JSPopupMenuItem extends ApplicationPopupMenuItem
/**
* Class for adding a separator (horizontal line, not selectable) the output
* will automatically reduce several consecutive separators to just one
*
* @package Extensibility
* @api
* @since 2.0
*/
class SeparatorPopupMenuItem extends ApplicationPopupMenuItem
{
static $idx = 0;
/**
* Class for inserting a separator into a popup menu
* Constructor
*/
public function __construct()
{
parent::__construct('', '');
parent::__construct('_separator_'.(self::$idx++), '');
}
/** @ignore */
public function GetMenuItem()
{
return array ('label' => '<hr class="menu-separator">', 'url' => '');
@@ -194,36 +489,520 @@ class SeparatorPopupMenuItem extends ApplicationPopupMenuItem
/**
* Implement this interface to add content to any iTopWebPage
*
* There are 3 places where content can be added:
* - The north pane: (normaly empty/hidden) at the top of the page, spanning the whole
*
* * The north pane: (normaly empty/hidden) at the top of the page, spanning the whole
* width of the page
* - The south pane: (normaly empty/hidden) at the bottom of the page, spanning the whole
* * The south pane: (normaly empty/hidden) at the bottom of the page, spanning the whole
* width of the page
* - The admin banner (two tones gray background) at the left of the global search.
* * The admin banner (two tones gray background) at the left of the global search.
* Limited space, use it for short messages
*
* Each of the methods of this interface is supposed to return the HTML to be inserted at
* the specified place and can use the passed iTopWebPage object to add javascript or CSS definitions
*
* @package Extensibility
* @api
* @since 2.0
*/
interface iPageUIExtension
{
/**
* Add content to the North pane
* @param WebPage $oPage The page to insert stuff into.
* @param iTopWebPage $oPage The page to insert stuff into.
* @return string The HTML content to add into the page
*/
public function GetNorthPaneHtml(iTopWebPage $oPage);
/**
* Add content to the South pane
* @param WebPage $oPage The page to insert stuff into.
* @param iTopWebPage $oPage The page to insert stuff into.
* @return string The HTML content to add into the page
*/
public function GetSouthPaneHtml(iTopWebPage $oPage);
/**
* Add content to the "admin banner"
* @param WebPage $oPage The page to insert stuff into.
* @param iTopWebPage $oPage The page to insert stuff into.
* @return string The HTML content to add into the page
*/
public function GetBannerHtml(iTopWebPage $oPage);
}
/**
* Implement this interface to add new operations to the REST/JSON web service
*
* @package Extensibility
* @api
* @since 2.0.1
*/
interface iRestServiceProvider
{
/**
* Enumerate services delivered by this class
* @param string $sVersion The version (e.g. 1.0) supported by the services
* @return array An array of hash 'verb' => verb, 'description' => description
*/
public function ListOperations($sVersion);
/**
* Enumerate services delivered by this class
* @param string $sVersion The version (e.g. 1.0) supported by the services
* @return RestResult The standardized result structure (at least a message)
* @throws Exception in case of internal failure.
*/
public function ExecOperation($sVersion, $sVerb, $aParams);
}
/**
* Minimal REST response structure. Derive this structure to add response data and error codes.
*
* @package Extensibility
* @api
* @since 2.0.1
*/
class RestResult
{
/**
* Result: no issue has been encountered
*/
const OK = 0;
/**
* Result: missing/wrong credentials or the user does not have enough rights to perform the requested operation
*/
const UNAUTHORIZED = 1;
/**
* Result: the parameter 'version' is missing
*/
const MISSING_VERSION = 2;
/**
* Result: the parameter 'json_data' is missing
*/
const MISSING_JSON = 3;
/**
* Result: the input structure is not a valid JSON string
*/
const INVALID_JSON = 4;
/**
* Result: the parameter 'auth_user' is missing, authentication aborted
*/
const MISSING_AUTH_USER = 5;
/**
* Result: the parameter 'auth_pwd' is missing, authentication aborted
*/
const MISSING_AUTH_PWD = 6;
/**
* Result: no operation is available for the specified version
*/
const UNSUPPORTED_VERSION = 10;
/**
* Result: the requested operation is not valid for the specified version
*/
const UNKNOWN_OPERATION = 11;
/**
* Result: the requested operation cannot be performed because it can cause data (integrity) loss
*/
const UNSAFE = 12;
/**
* Result: the operation could not be performed, see the message for troubleshooting
*/
const INTERNAL_ERROR = 100;
/**
* Default constructor - ok!
*
* @param DBObject $oObject The object being reported
* @param string $sAttCode The attribute code (must be valid)
* @return string A scalar representation of the value
*/
public function __construct()
{
$this->code = RestResult::OK;
}
public $code;
public $message;
}
/**
* Helpers for implementing REST services
*
* @package Extensibility
* @api
*/
class RestUtils
{
/**
* Registering tracking information. Any further object modification be associated with the given comment, when the modification gets recorded into the DB
*
* @param StdClass $oData Structured input data. Must contain 'comment'.
* @return void
* @throws Exception
* @api
*/
public static function InitTrackingComment($oData)
{
$sComment = self::GetMandatoryParam($oData, 'comment');
CMDBObject::SetTrackInfo($sComment);
}
/**
* Read a mandatory parameter from from a Rest/Json structure.
*
* @param StdClass $oData Structured input data. Must contain the entry defined by sParamName.
* @param string $sParamName Name of the parameter to fetch from the input data
* @return void
* @throws Exception If the parameter is missing
* @api
*/
public static function GetMandatoryParam($oData, $sParamName)
{
if (isset($oData->$sParamName))
{
return $oData->$sParamName;
}
else
{
throw new Exception("Missing parameter '$sParamName'");
}
}
/**
* Read an optional parameter from from a Rest/Json structure.
*
* @param StdClass $oData Structured input data.
* @param string $sParamName Name of the parameter to fetch from the input data
* @param mixed $default Default value if the parameter is not found in the input data
* @return void
* @throws Exception
* @api
*/
public static function GetOptionalParam($oData, $sParamName, $default)
{
if (isset($oData->$sParamName))
{
return $oData->$sParamName;
}
else
{
return $default;
}
}
/**
* Read a class from a Rest/Json structure.
*
* @param StdClass $oData Structured input data. Must contain the entry defined by sParamName.
* @param string $sParamName Name of the parameter to fetch from the input data
* @return void
* @throws Exception If the parameter is missing or the class is unknown
* @api
*/
public static function GetClass($oData, $sParamName)
{
$sClass = self::GetMandatoryParam($oData, $sParamName);
if (!MetaModel::IsValidClass($sClass))
{
throw new Exception("$sParamName: '$sClass' is not a valid class'");
}
return $sClass;
}
/**
* Read a list of attribute codes from a Rest/Json structure.
*
* @param string $sClass Name of the class
* @param StdClass $oData Structured input data.
* @param string $sParamName Name of the parameter to fetch from the input data
* @return An array of class => list of attributes (see RestResultWithObjects::AddObject that uses it)
* @throws Exception
* @api
*/
public static function GetFieldList($sClass, $oData, $sParamName)
{
$sFields = self::GetOptionalParam($oData, $sParamName, '*');
$aShowFields = array();
if ($sFields == '*')
{
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
{
$aShowFields[$sClass][] = $sAttCode;
}
}
elseif ($sFields == '*+')
{
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass)
{
foreach (MetaModel::ListAttributeDefs($sRefClass) as $sAttCode => $oAttDef)
{
$aShowFields[$sRefClass][] = $sAttCode;
}
}
}
else
{
foreach(explode(',', $sFields) as $sAttCode)
{
$sAttCode = trim($sAttCode);
if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode)))
{
throw new Exception("$sParamName: invalid attribute code '$sAttCode'");
}
$aShowFields[$sClass][] = $sAttCode;
}
}
return $aShowFields;
}
/**
* Read and interpret object search criteria from a Rest/Json structure
*
* @param string $sClass Name of the class
* @param StdClass $oCriteria Hash of attribute code => value (can be a substructure or a scalar, depending on the nature of the attriute)
* @return object The object found
* @throws Exception If the input structure is not valid or it could not find exactly one object
*/
protected static function FindObjectFromCriteria($sClass, $oCriteria)
{
$aCriteriaReport = array();
if (isset($oCriteria->finalclass))
{
if (!MetaModel::IsValidClass($oCriteria->finalclass))
{
throw new Exception("finalclass: Unknown class '".$oCriteria->finalclass."'");
}
if (!MetaModel::IsParentClass($sClass, $oCriteria->finalclass))
{
throw new Exception("finalclass: '".$oCriteria->finalclass."' is not a child class of '$sClass'");
}
$sClass = $oCriteria->finalclass;
}
$oSearch = new DBObjectSearch($sClass);
foreach ($oCriteria as $sAttCode => $value)
{
$realValue = self::MakeValue($sClass, $sAttCode, $value);
$oSearch->AddCondition($sAttCode, $realValue, '=');
$aCriteriaReport[] = "$sAttCode: $value ($realValue)";
}
$oSet = new DBObjectSet($oSearch);
$iCount = $oSet->Count();
if ($iCount == 0)
{
throw new Exception("No item found with criteria: ".implode(', ', $aCriteriaReport));
}
elseif ($iCount > 1)
{
throw new Exception("Several items found ($iCount) with criteria: ".implode(', ', $aCriteriaReport));
}
$res = $oSet->Fetch();
return $res;
}
/**
* Find an object from a polymorph search specification (Rest/Json)
*
* @param string $sClass Name of the class
* @param mixed $key Either search criteria (substructure), or an object or an OQL string.
* @param bool $bAllowNullValue Allow the cases such as key = 0 or key = {null} and return null then
* @return DBObject The object found
* @throws Exception If the input structure is not valid or it could not find exactly one object
* @api
*/
public static function FindObjectFromKey($sClass, $key, $bAllowNullValue = false)
{
if (is_object($key))
{
$res = self::FindObjectFromCriteria($sClass, $key);
}
elseif (is_numeric($key))
{
if ($bAllowNullValue && ($key == 0))
{
$res = null;
}
else
{
$res = MetaModel::GetObject($sClass, $key, false);
if (is_null($res))
{
throw new Exception("Invalid object $sClass::$key");
}
}
}
elseif (is_string($key))
{
// OQL
$oSearch = DBObjectSearch::FromOQL($key);
$oSet = new DBObjectSet($oSearch);
$iCount = $oSet->Count();
if ($iCount == 0)
{
throw new Exception("No item found for query: $key");
}
elseif ($iCount > 1)
{
throw new Exception("Several items found ($iCount) for query: $key");
}
$res = $oSet->Fetch();
}
else
{
throw new Exception("Wrong format for key");
}
return $res;
}
/**
* Search objects from a polymorph search specification (Rest/Json)
*
* @param string $sClass Name of the class
* @param mixed $key Either search criteria (substructure), or an object or an OQL string.
* @return DBObjectSet The search result set
* @throws Exception If the input structure is not valid
*/
public static function GetObjectSetFromKey($sClass, $key)
{
if (is_object($key))
{
if (isset($key->finalclass))
{
$sClass = $key->finalclass;
if (!MetaModel::IsValidClass($sClass))
{
throw new Exception("finalclass: Unknown class '$sClass'");
}
}
$oSearch = new DBObjectSearch($sClass);
foreach ($key as $sAttCode => $value)
{
$realValue = self::MakeValue($sClass, $sAttCode, $value);
$oSearch->AddCondition($sAttCode, $realValue, '=');
}
}
elseif (is_numeric($key))
{
$oSearch = new DBObjectSearch($sClass);
$oSearch->AddCondition('id', $key);
}
elseif (is_string($key))
{
// OQL
$oSearch = DBObjectSearch::FromOQL($key);
$oObjectSet = new DBObjectSet($oSearch);
}
else
{
throw new Exception("Wrong format for key");
}
$oObjectSet = new DBObjectSet($oSearch);
return $oObjectSet;
}
/**
* Interpret the Rest/Json value and get a valid attribute value
*
* @param string $sClass Name of the class
* @param string $sAttCode Attribute code
* @param mixed $value Depending on the type of attribute (a scalar, or search criteria, or list of related objects...)
* @return mixed The value that can be used with DBObject::Set()
* @throws Exception If the specification of the value is not valid.
* @api
*/
public static function MakeValue($sClass, $sAttCode, $value)
{
try
{
if (!MetaModel::IsValidAttCode($sClass, $sAttCode))
{
throw new Exception("Unknown attribute");
}
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
if ($oAttDef instanceof AttributeExternalKey)
{
$oExtKeyObject = self::FindObjectFromKey($oAttDef->GetTargetClass(), $value, true /* allow null */);
$value = ($oExtKeyObject != null) ? $oExtKeyObject->GetKey() : 0;
}
elseif ($oAttDef instanceof AttributeLinkedSet)
{
if (!is_array($value))
{
throw new Exception("A link set must be defined by an array of objects");
}
$sLnkClass = $oAttDef->GetLinkedClass();
$aLinks = array();
foreach($value as $oValues)
{
$oLnk = self::MakeObjectFromFields($sLnkClass, $oValues);
$aLinks[] = $oLnk;
}
$value = DBObjectSet::FromArray($sLnkClass, $aLinks);
}
else
{
$value = $oAttDef->FromJSONToValue($value);
}
}
catch (Exception $e)
{
throw new Exception("$sAttCode: ".$e->getMessage(), $e->getCode());
}
return $value;
}
/**
* Interpret a Rest/Json structure that defines attribute values, and build an object
*
* @param string $sClass Name of the class
* @param array $aFields A hash of attribute code => value specification.
* @return DBObject The newly created object
* @throws Exception If the specification of the values is not valid
* @api
*/
public static function MakeObjectFromFields($sClass, $aFields)
{
$oObject = MetaModel::NewObject($sClass);
foreach ($aFields as $sAttCode => $value)
{
$realValue = self::MakeValue($sClass, $sAttCode, $value);
try
{
$oObject->Set($sAttCode, $realValue);
}
catch (Exception $e)
{
throw new Exception("$sAttCode: ".$e->getMessage(), $e->getCode());
}
}
return $oObject;
}
/**
* Interpret a Rest/Json structure that defines attribute values, and update the given object
*
* @param DBObject $oObject The object being modified
* @param array $aFields A hash of attribute code => value specification.
* @return DBObject The object modified
* @throws Exception If the specification of the values is not valid
* @api
*/
public static function UpdateObjectFromFields($oObject, $aFields)
{
$sClass = get_class($oObject);
foreach ($aFields as $sAttCode => $value)
{
$realValue = self::MakeValue($sClass, $sAttCode, $value);
try
{
$oObject->Set($sAttCode, $realValue);
}
catch (Exception $e)
{
throw new Exception("$sAttCode: ".$e->getMessage(), $e->getCode());
}
}
return $oObject;
}
}

View File

@@ -39,6 +39,10 @@ class CLIPage implements Page
{
MetaModel::RecordQueryTrace();
}
if (class_exists('ExecutionKPI'))
{
ExecutionKPI::ReportStats();
}
}
public function add($sText)

File diff suppressed because it is too large Load Diff

View File

@@ -51,6 +51,10 @@ class CSVPage extends WebPage
{
MetaModel::RecordQueryTrace();
}
if (class_exists('ExecutionKPI'))
{
ExecutionKPI::ReportStats();
}
}
public function small_p($sText)

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -18,6 +18,7 @@
require_once(APPROOT.'application/dashboardlayout.class.inc.php');
require_once(APPROOT.'application/dashlet.class.inc.php');
require_once(APPROOT.'core/modelreflection.class.inc.php');
/**
* A user editable dashboard page
@@ -28,20 +29,26 @@ require_once(APPROOT.'application/dashlet.class.inc.php');
abstract class Dashboard
{
protected $sTitle;
protected $bAutoReload;
protected $iAutoReloadSec;
protected $sLayoutClass;
protected $aWidgetsData;
protected $oDOMNode;
protected $sId;
protected $aCells;
protected $oMetaModel;
public function __construct($sId)
{
$this->sLayoutClass = null;
$this->sTitle = '';
$this->sLayoutClass = 'DashboardLayoutOneCol';
$this->bAutoReload = false;
$this->iAutoReloadSec = MetaModel::GetConfig()->GetStandardReloadInterval();
$this->aCells = array();
$this->oDOMNode = null;
$this->sId = $sId;
}
public function FromXml($sXml)
{
$this->aCells = array(); // reset the content of the dashboard
@@ -49,60 +56,97 @@ abstract class Dashboard
$oDoc = new DOMDocument();
$oDoc->loadXML($sXml);
restore_error_handler();
$this->oDOMNode = $oDoc->getElementsByTagName('dashboard')->item(0);
$oLayoutNode = $this->oDOMNode->getElementsByTagName('layout')->item(0);
$this->sLayoutClass = $oLayoutNode->textContent;
$oTitleNode = $this->oDOMNode->getElementsByTagName('title')->item(0);
$this->sTitle = $oTitleNode->textContent;
$oCellsNode = $this->oDOMNode->getElementsByTagName('cells')->item(0);
$oCellsList = $oCellsNode->getElementsByTagName('cell');
$aCellOrder = array();
$iCellRank = 0;
foreach($oCellsList as $oCellNode)
{
$aDashletList = array();
$oCellRank = $oCellNode->getElementsByTagName('rank')->item(0);
if ($oCellRank)
{
$iCellRank = (float)$oCellRank->textContent;
}
$oDashletsNode = $oCellNode->getElementsByTagName('dashlets')->item(0);
$oDashletList = $oDashletsNode->getElementsByTagName('dashlet');
$iRank = 0;
$aDashletOrder = array();
foreach($oDashletList as $oDomNode)
{
$sDashletClass = $oDomNode->getAttribute('xsi:type');
$oRank = $oDomNode->getElementsByTagName('rank')->item(0);
if ($oRank)
{
$iRank = (float)$oRank->textContent;
}
$sId = $oDomNode->getAttribute('id');
$oNewDashlet = new $sDashletClass($sId);
$oNewDashlet->FromDOMNode($oDomNode);
$aDashletOrder[] = array('rank' => $iRank, 'dashlet' => $oNewDashlet);
}
usort($aDashletOrder, array(get_class($this), 'SortOnRank'));
$aDashletList = array();
foreach($aDashletOrder as $aItem)
{
$aDashletList[] = $aItem['dashlet'];
}
$aCellOrder[] = array('rank' => $iCellRank, 'dashlets' => $aDashletList);
}
usort($aCellOrder, array(get_class($this), 'SortOnRank'));
foreach($aCellOrder as $aItem)
{
$this->aCells[] = $aItem['dashlets'];
}
$this->FromDOMDocument($oDoc);
}
public function FromDOMDocument(DOMDocument $oDoc)
{
$this->oDOMNode = $oDoc->getElementsByTagName('dashboard')->item(0);
if ($oLayoutNode = $this->oDOMNode->getElementsByTagName('layout')->item(0))
{
$this->sLayoutClass = $oLayoutNode->textContent;
}
else
{
$this->sLayoutClass = 'DashboardLayoutOneCol';
}
if ($oTitleNode = $this->oDOMNode->getElementsByTagName('title')->item(0))
{
$this->sTitle = $oTitleNode->textContent;
}
else
{
$this->sTitle = '';
}
$this->bAutoReload = false;
$this->iAutoReloadSec = MetaModel::GetConfig()->GetStandardReloadInterval();
if ($oAutoReloadNode = $this->oDOMNode->getElementsByTagName('auto_reload')->item(0))
{
if ($oAutoReloadEnabled = $oAutoReloadNode->getElementsByTagName('enabled')->item(0))
{
$this->bAutoReload = ($oAutoReloadEnabled->textContent == 'true');
}
if ($oAutoReloadInterval = $oAutoReloadNode->getElementsByTagName('interval')->item(0))
{
$this->iAutoReloadSec = max(5, (int)$oAutoReloadInterval->textContent);
}
}
if ($oCellsNode = $this->oDOMNode->getElementsByTagName('cells')->item(0))
{
$oCellsList = $oCellsNode->getElementsByTagName('cell');
$aCellOrder = array();
$iCellRank = 0;
foreach($oCellsList as $oCellNode)
{
$aDashletList = array();
$oCellRank = $oCellNode->getElementsByTagName('rank')->item(0);
if ($oCellRank)
{
$iCellRank = (float)$oCellRank->textContent;
}
$oDashletsNode = $oCellNode->getElementsByTagName('dashlets')->item(0);
{
$oDashletList = $oDashletsNode->getElementsByTagName('dashlet');
$iRank = 0;
$aDashletOrder = array();
foreach($oDashletList as $oDomNode)
{
$sDashletClass = $oDomNode->getAttribute('xsi:type');
$oRank = $oDomNode->getElementsByTagName('rank')->item(0);
if ($oRank)
{
$iRank = (float)$oRank->textContent;
}
$sId = $oDomNode->getAttribute('id');
$oNewDashlet = new $sDashletClass($this->oMetaModel, $sId);
$oNewDashlet->FromDOMNode($oDomNode);
$aDashletOrder[] = array('rank' => $iRank, 'dashlet' => $oNewDashlet);
}
usort($aDashletOrder, array(get_class($this), 'SortOnRank'));
$aDashletList = array();
foreach($aDashletOrder as $aItem)
{
$aDashletList[] = $aItem['dashlet'];
}
$aCellOrder[] = array('rank' => $iCellRank, 'dashlets' => $aDashletList);
}
}
usort($aCellOrder, array(get_class($this), 'SortOnRank'));
foreach($aCellOrder as $aItem)
{
$this->aCells[] = $aItem['dashlets'];
}
}
else
{
$this->aCells = array();
}
}
static function SortOnRank($aItem1, $aItem2)
{
return ($aItem1['rank'] > $aItem2['rank']) ? +1 : -1;
@@ -132,14 +176,31 @@ abstract class Dashboard
$oMainNode->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance");
$oDoc->appendChild($oMainNode);
$this->ToDOMNode($oMainNode);
$sXml = $oDoc->saveXML();
return $sXml;
}
public function ToDOMNode($oDefinition)
{
$oDoc = $oDefinition->ownerDocument;
$oNode = $oDoc->createElement('layout', $this->sLayoutClass);
$oMainNode->appendChild($oNode);
$oDefinition->appendChild($oNode);
$oNode = $oDoc->createElement('title', $this->sTitle);
$oMainNode->appendChild($oNode);
$oDefinition->appendChild($oNode);
$oAutoReloadNode = $oDoc->createElement('auto_reload');
$oDefinition->appendChild($oAutoReloadNode);
$oNode = $oDoc->createElement('enabled', $this->bAutoReload ? 'true' : 'false');
$oAutoReloadNode->appendChild($oNode);
$oNode = $oDoc->createElement('interval', $this->iAutoReloadSec);
$oAutoReloadNode->appendChild($oNode);
$oCellsNode = $oDoc->createElement('cells');
$oMainNode->appendChild($oCellsNode);
$oDefinition->appendChild($oCellsNode);
$iCellRank = 0;
foreach ($this->aCells as $aCell)
@@ -166,15 +227,15 @@ abstract class Dashboard
$oDashlet->ToDOMNode($oNode);
}
}
$sXml = $oDoc->saveXML();
return $sXml;
}
public function FromParams($aParams)
{
$this->sLayoutClass = $aParams['layout_class'];
$this->sTitle = $aParams['title'];
$this->bAutoReload = $aParams['auto_reload'] == 'true';
$this->iAutoReloadSec = max(5, (int) $aParams['auto_reload_sec']);
foreach($aParams['cells'] as $aCell)
{
@@ -183,7 +244,7 @@ abstract class Dashboard
{
$sDashletClass = $aDashletParams['dashlet_class'];
$sId = $aDashletParams['dashlet_id'];
$oNewDashlet = new $sDashletClass($sId);
$oNewDashlet = new $sDashletClass($this->oMetaModel, $sId);
$oForm = $oNewDashlet->GetForm();
$oForm->SetParamsContainer($sId);
@@ -196,7 +257,7 @@ abstract class Dashboard
}
}
public function Save()
{
@@ -216,12 +277,32 @@ abstract class Dashboard
{
return $this->sTitle;
}
public function SetTitle($sTitle)
{
$this->sTitle = $sTitle;
}
public function GetAutoReload()
{
return $this->bAutoReload;
}
public function SetAutoReload($bAutoReload)
{
$this->bAutoReload = $bAutoReload;
}
public function GetAutoReloadInterval()
{
return $this->iAutoReloadSec;
}
public function SetAutoReloadInterval($iAutoReloadSec)
{
$this->iAutoReloadSec = max(5, (int)$iAutoReloadSec);
}
public function AddDashlet($oDashlet)
{
$sId = $this->GetNewDashletId();
@@ -266,24 +347,61 @@ abstract class Dashboard
$oPage->add('</div>');
$oForm = new DesignerForm();
$oField = new DesignerHiddenField('dashboard_id', '', $this->sId);
$oForm->AddField($oField);
$oField = new DesignerLongTextField('dashboard_title', Dict::S('UI:DashboardEdit:DashboardTitle'), $this->sTitle);
$oForm->AddField($oField);
$oField = new DesignerBooleanField('auto_reload', Dict::S('UI:DashboardEdit:AutoReload'), $this->bAutoReload);
$oForm->AddField($oField);
$oField = new DesignerTextField('auto_reload_sec', Dict::S('UI:DashboardEdit:AutoReloadSec'), $this->iAutoReloadSec);
$oField->SetValidationPattern('^$|^0*([5-9]|[1-9][0-9]+)$'); // Can be empty, or a number > 4
$oForm->AddField($oField);
$this->SetFormParams($oForm);
$oForm->RenderAsPropertySheet($oPage, false, ':itop-dashboard');
$oForm->RenderAsPropertySheet($oPage, false, '.itop-dashboard');
$oPage->add('</div>');
$sRateTitle = addslashes(Dict::S('UI:DashboardEdit:AutoReloadSec+'));
$oPage->add_ready_script(
<<<EOF
$('#select_layout').buttonset();
$('#select_layout input').click( function() {
var sLayoutClass = $(this).val();
$(':itop-dashboard').dashboard('option', {layout_class: sLayoutClass});
// Note: the title gets deleted by the validation mechanism
$("#attr_auto_reload_sec").tooltip({items: 'input', content: '$sRateTitle'});
$("#attr_auto_reload_sec").prop('disabled', !$('#attr_auto_reload').is(':checked'));
$('#attr_auto_reload').change( function(ev) {
$("#attr_auto_reload_sec").prop('disabled', !$(this).is(':checked'));
} );
$('#row_attr_dashboard_title').property_field('option', {parent_selector: ':itop-dashboard', auto_apply: false, 'do_apply': function() {
var sTitle = $('#attr_dashboard_title').val();
$(':itop-dashboard').dashboard('option', {title: sTitle});
return true;
}
$('#select_layout').buttonset();
$('#select_dashlet').droppable({
accept: '.dashlet',
drop: function(event, ui) {
$( this ).find( ".placeholder" ).remove();
var oDashlet = ui.draggable.data('itopDashlet');
oDashlet._remove_dashlet();
},
});
$('#event_bus').bind('dashlet-selected', function(event, data){
var sDashletId = data.dashlet_id;
var sPropId = 'dashlet_properties_'+sDashletId;
$('.dashlet_properties').each(function() {
var sId = $(this).attr('id');
var bShow = (sId == sPropId);
if (bShow)
{
$(this).show();
}
else
{
$(this).hide();
}
});
});
EOF
);
@@ -318,7 +436,6 @@ EOF
$oPage->add('</div>');
$oPage->add_ready_script("$('.dashlet_icon').draggable({helper: 'clone', appendTo: 'body', zIndex: 10000, revert:'invalid'});");
$oPage->add_ready_script("$('.layout_cell').droppable({accept:'.dashlet_icon', hoverClass:'dragHover'});");
}
public function RenderDashletsProperties($oPage)
@@ -338,7 +455,7 @@ EOF
$oPage->add('<div class="dashlet_properties" id="dashlet_properties_'.$sId.'" style="display:none">');
$oForm = $oDashlet->GetForm();
$this->SetFormParams($oForm);
$oForm->RenderAsPropertySheet($oPage, false, ':itop-dashboard');
$oForm->RenderAsPropertySheet($oPage, false, '.itop-dashboard');
$oPage->add('</div>');
}
}
@@ -367,11 +484,12 @@ EOF
class RuntimeDashboard extends Dashboard
{
protected $bCustomized;
public function __construct($sId)
{
parent::__construct($sId);
$this->bCustomized = false;
$this->oMetaModel = new ModelReflectionRuntime();
}
public function SetCustomFlag($bCustomized)
@@ -425,37 +543,34 @@ class RuntimeDashboard extends Dashboard
}
}
public function Render($oPage, $bEditMode = false, $aExtraParams = array())
public function RenderEditionTools($oPage)
{
parent::Render($oPage, $bEditMode, $aExtraParams);
if (!$bEditMode)
$sEditMenu = "<td><span id=\"DashboardMenu\"><ul><li><img src=\"../images/edit.png\"><ul>";
$aActions = array();
$oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:Edit'), "return EditDashboard('{$this->sId}')");
$aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
if ($this->bCustomized)
{
$sEditMenu = "<td><span id=\"DashboardMenu\"><ul><li><img src=\"../images/edit.png\"><ul>";
$aActions = array();
$oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:Edit'), "return EditDashboard('{$this->sId}')");
$aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
$oRevert = new JSPopupMenuItem('UI:Dashboard:RevertConfirm', Dict::S('UI:Dashboard:Revert'),
"if (confirm('".addslashes(Dict::S('UI:Dashboard:RevertConfirm'))."')) return RevertDashboard('{$this->sId}'); else return false");
$aActions[$oRevert->GetUID()] = $oRevert->GetMenuItem();
}
utils::GetPopupMenuItems($oPage, iPopupMenuExtension::MENU_DASHBOARD_ACTIONS, $this, $aActions);
$sEditMenu .= $oPage->RenderPopupMenuItems($aActions);
if ($this->bCustomized)
{
$oRevert = new JSPopupMenuItem('UI:Dashboard:RevertConfirm', Dict::S('UI:Dashboard:Revert'),
"if (confirm('".addslashes(Dict::S('UI:Dashboard:RevertConfirm'))."')) return RevertDashboard('{$this->sId}'); else return false");
$aActions[$oRevert->GetUID()] = $oRevert->GetMenuItem();
}
utils::GetPopupMenuItems($oPage, iPopupMenuExtension::MENU_DASHBOARD_ACTIONS, $this, $aActions);
$sEditMenu .= $oPage->RenderPopupMenuItems($aActions);
$sEditMenu = addslashes($sEditMenu);
//$sEditBtn = addslashes('<div style="display: inline-block; height: 55px; width:200px;vertical-align:center;line-height:60px;text-align:left;"><button onclick="EditDashboard(\''.$this->sId.'\');">Edit This Page</button></div>');
$oPage->add_ready_script(
$sEditMenu = addslashes($sEditMenu);
//$sEditBtn = addslashes('<div style="display: inline-block; height: 55px; width:200px;vertical-align:center;line-height:60px;text-align:left;"><button onclick="EditDashboard(\''.$this->sId.'\');">Edit This Page</button></div>');
$oPage->add_ready_script(
<<<EOF
$('#logOffBtn').parent().before('$sEditMenu');
$('#DashboardMenu>ul').popupmenu();
EOF
);
$oPage->add_script(
);
$oPage->add_script(
<<<EOF
function EditDashboard(sId)
{
@@ -478,10 +593,42 @@ function RevertDashboard(sId)
return false;
}
EOF
);
}
);
}
public function RenderProperties($oPage)
{
parent::RenderProperties($oPage);
$oPage->add_ready_script(
<<<EOF
$('#select_layout input').click( function() {
var sLayoutClass = $(this).val();
$('.itop-dashboard').runtimedashboard('option', {layout_class: sLayoutClass});
} );
$('#row_attr_dashboard_title').property_field('option', {parent_selector: '.itop-dashboard', auto_apply: false, 'do_apply': function() {
var sTitle = $('#attr_dashboard_title').val();
$('.itop-dashboard').runtimedashboard('option', {title: sTitle});
return true;
}
});
$('#row_attr_auto_reload').property_field('option', {parent_selector: '.itop-dashboard', auto_apply: true, 'do_apply': function() {
var bAutoReload = $('#attr_auto_reload').is(':checked');
$('.itop-dashboard').runtimedashboard('option', {auto_reload: bAutoReload});
return true;
}
});
$('#row_attr_auto_reload_sec').property_field('option', {parent_selector: '.itop-dashboard', auto_apply: true, 'do_apply': function() {
var iAutoReloadSec = $('#attr_auto_reload_sec').val();
$('.itop-dashboard').runtimedashboard('option', {auto_reload_sec: iAutoReloadSec});
return true;
}
});
EOF
);
}
public function RenderEditor($oPage)
{
$oPage->add('<div id="dashboard_editor">');
@@ -502,6 +649,8 @@ EOF
$sId = addslashes($this->sId);
$sLayoutClass = addslashes($this->sLayoutClass);
$sAutoReload = $this->bAutoReload ? 'true' : 'false';
$sAutoReloadSec = (string) $this->iAutoReloadSec;
$sTitle = addslashes($this->sTitle);
$sUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
@@ -520,7 +669,7 @@ $('#dashboard_editor').dialog({
title: '$sDialogTitle',
buttons: [
{ text: "$sOkButtonLabel", click: function() {
var oDashboard = $(':itop-dashboard').data('dashboard');
var oDashboard = $('.itop-dashboard').data('itopRuntimedashboard');
if (oDashboard.is_dirty())
{
if (!confirm('$sAutoApplyConfirmationMessage'))
@@ -536,7 +685,7 @@ $('#dashboard_editor').dialog({
oDashboard.save();
} },
{ text: "$sCancelButtonLabel", click: function() {
var oDashboard = $(':itop-dashboard').data('dashboard');
var oDashboard = $('.itop-dashboard').data('itopRuntimedashboard');
if (oDashboard.is_modified())
{
if (!confirm('$sCancelConfirmationMessage'))
@@ -552,40 +701,14 @@ $('#dashboard_editor').dialog({
close: function() { $(this).remove(); }
});
$('#dashboard_editor .ui-layout-center').dashboard({
$('#dashboard_editor .ui-layout-center').runtimedashboard({
dashboard_id: '$sId', layout_class: '$sLayoutClass', title: '$sTitle',
auto_reload: $sAutoReload, auto_reload_sec: $sAutoReloadSec,
submit_to: '$sUrl', submit_parameters: {operation: 'save_dashboard'},
render_to: '$sUrl', render_parameters: {operation: 'render_dashboard'},
new_dashlet_parameters: {operation: 'new_dashlet'}
});
$('#select_dashlet').droppable({
accept: '.dashlet',
drop: function(event, ui) {
$( this ).find( ".placeholder" ).remove();
var oDashlet = ui.draggable;
oDashlet.remove();
},
});
$('#event_bus').bind('dashlet-selected', function(event, data){
var sDashletId = data.dashlet_id;
var sPropId = 'dashlet_properties_'+sDashletId;
$('.dashlet_properties').each(function() {
var sId = $(this).attr('id');
var bShow = (sId == sPropId);
if (bShow)
{
$(this).show();
}
else
{
$(this).hide();
}
});
});
dashboard_prop_size = GetUserPreference('dashboard_prop_size', 350);
$('#dashboard_editor').layout({
east: {
@@ -606,7 +729,7 @@ $('#dashboard_editor').layout({
window.onbeforeunload = function() {
if (!window.bLeavingOnUserAction)
{
var oDashboard = $(':itop-dashboard').data('dashboard');
var oDashboard = $('.itop-dashboard').data('itopRuntimedashboard');
if (oDashboard)
{
if (oDashboard.is_dirty())
@@ -687,7 +810,8 @@ EOF
foreach($aDashlets as $sDashletClass => $aDashletInfo)
{
$oSubForm = new DesignerForm();
$oDashlet = new $sDashletClass(0);
$oMetaModel = new ModelReflectionRuntime();
$oDashlet = new $sDashletClass($oMetaModel, 0);
$oDashlet->GetPropertiesFieldsFromOQL($oSubForm, $sOQL);
$oSelectorField->AddSubForm($oSubForm, $aDashletInfo['label'], $aDashletInfo['class']);

View File

@@ -107,14 +107,16 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
$oPage->add('<table style="width:100%"><tbody>');
$iCellIdx = 0;
$fColSize = 100 / $this->iNbCols;
$sStyle = $bEditMode ? 'style="border: 1px #ccc dashed; width:'.$fColSize.'%;" class="layout_cell edit_mode"' : 'style="width: '.$fColSize.'%;" class="dashboard"';
$sStyle = $bEditMode ? 'border: 1px #ccc dashed; width:'.$fColSize.'%;' : 'width: '.$fColSize.'%;';
$sClass = $bEditMode ? 'layout_cell edit_mode' : 'dashboard';
$iNbRows = ceil(count($aCells) / $this->iNbCols);
for($iRows = 0; $iRows < $iNbRows; $iRows++)
{
$oPage->add('<tr>');
for($iCols = 0; $iCols < $this->iNbCols; $iCols++)
{
$oPage->add("<td $sStyle>");
$sCellClass = ($iRows == $iNbRows-1) ? $sClass.' layout_last_used_rank' : $sClass;
$oPage->add("<td style=\"$sStyle\" class=\"$sCellClass\" data-dashboard-cell-index=\"$iCellIdx\">");
if (array_key_exists($iCellIdx, $aCells))
{
$aDashlets = $aCells[$iCellIdx];
@@ -144,7 +146,7 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
}
if ($bEditMode) // Add one row for extensibility
{
$sStyle = 'style="border: 1px #ccc dashed; width:'.$fColSize.'%;" class="layout_cell edit_mode layout_extension"';
$sStyle = 'style="border: 1px #ccc dashed; width:'.$fColSize.'%;" class="layout_cell edit_mode layout_extension" data-dashboard-cell-index="'.$iCellIdx.'"';
$oPage->add('<tr>');
for($iCols = 0; $iCols < $this->iNbCols; $iCols++)
{

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -60,6 +60,10 @@ class DataTable
{
// Custom settings overload the default ones
$this->bUseCustomSettings = true;
if ($this->oDefaultSettings->iDefaultPageSize == 0)
{
$oCustomSettings->iDefaultPageSize = 0;
}
}
else
{
@@ -71,7 +75,35 @@ class DataTable
$this->oSet->SetLimit($oCustomSettings->iDefaultPageSize);
}
$this->oSet->SetOrderBy($oCustomSettings->GetSortOrder());
// Load only the requested columns
$aColumnsToLoad = array();
foreach($oCustomSettings->aColumns as $sAlias => $aColumnsInfo)
{
foreach($aColumnsInfo as $sAttCode => $aData)
{
if ($sAttCode != '_key_')
{
if ($aData['checked'])
{
$aColumnsToLoad[$sAlias][] = $sAttCode;
}
else
{
// See if this column is a must to load
$sClass = $this->aClassAliases[$sAlias];
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
if ($oAttDef->alwaysLoadInTables())
{
$aColumnsToLoad[$sAlias][] = $sAttCode;
}
}
}
}
}
$this->oSet->OptimizeColumnLoad($aColumnsToLoad);
$bToolkitMenu = true;
if (isset($aExtraParams['toolkit_menu']))
{
@@ -176,6 +208,8 @@ class DataTable
if ($iPageSize < 1) // Display all
{
$sPagerStyle = 'style="display:none"'; // no limit: display the full table, so hide the "pager" UI
// WARNING: mPDF does not take the "display" style into account
// when applied to a <td> or a <table> tag, so make sure you apply this to a div
}
else
{
@@ -226,7 +260,8 @@ class DataTable
$sSelectionMode = ($iNbPages == 1) ? '' : 'positive';
$sHtml =
<<<EOF
<td $sPagerStyle colspan="2">
<td colspan="2">
<div $sPagerStyle>
<table id="pager{$this->iListId}" class="pager"><tr>
<td>$sPages</td>
<td><img src="../images/first.png" class="first"/></td>
@@ -239,6 +274,7 @@ class DataTable
</td>
</tr>
</table>
</div>
</td>
EOF;
return $sHtml;
@@ -256,13 +292,13 @@ EOF;
{
$sMenuTitle = Dict::S('UI:ConfigureThisList');
$sHtml = '<div class="itop_popup toolkit_menu" id="tk_'.$this->iListId.'"><ul><li><img src="../images/toolkit_menu.png"><ul>';
$oMenuItem1 = new JSPopupMenuItem('iTop::ConfigureList', $sMenuTitle, "$('#datatable_dlg_".$this->iListId."').dialog('open');");
$aActions = array(
$oMenuItem1->GetUID() => $oMenuItem1->GetMenuItem(),
);
$this->oSet->Rewind();
utils::GetPopupMenuItems($oPage, iPopupMenuExtension::MENU_OBJLIST_TOOLKIT, $this->oSet, $aActions);
utils::GetPopupMenuItems($oPage, iPopupMenuExtension::MENU_OBJLIST_TOOLKIT, $this->oSet, $aActions, $this->sTableId, $this->iListId);
$this->oSet->Rewind();
$sHtml .= $oPage->RenderPopupMenuItems($aActions);
return $sHtml;
@@ -728,7 +764,7 @@ class DataTableSettings implements Serializable
}
}
static public function GetTableSettings($aClassAliases, $sTableId = null)
static public function GetTableSettings($aClassAliases, $sTableId = null, $bOnlyOnTable = false)
{
$pref = null;
$oSettings = new DataTableSettings($aClassAliases, $sTableId);
@@ -741,8 +777,11 @@ class DataTableSettings implements Serializable
if ($pref == null)
{
// Try the global preferred values for this class / set of classes
$pref = appUserPreferences::GetPref($oSettings->GetPrefsKey(null), null);
if (!$bOnlyOnTable)
{
// Try the global preferred values for this class / set of classes
$pref = appUserPreferences::GetPref($oSettings->GetPrefsKey(null), null);
}
if ($pref == null)
{
// no such settings, use the default values provided by the data model
@@ -772,12 +811,13 @@ class DataTableSettings implements Serializable
return $aSortOrder;
}
public function Save()
public function Save($sTargetTableId = null)
{
if ($this->sTableId == null) return false; // Cannot save, the table is not identified, use SaveAsDefault instead
$sSaveId = is_null($sTargetTableId) ? $this->sTableId : $sTargetTableId;
if ($sSaveId == null) return false; // Cannot save, the table is not identified, use SaveAsDefault instead
$sSettings = $this->serialize();
appUserPreferences::SetPref($this->GetPrefsKey($this->sTableId), $sSettings);
appUserPreferences::SetPref($this->GetPrefsKey($sSaveId), $sSettings);
return true;
}

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -59,6 +59,11 @@ class DisplayBlock
$this->m_aParams = $aParams;
$this->m_oSet = $oSet;
}
public function GetFilter()
{
return $this->m_oFilter;
}
/**
* Constructs a DisplayBlock object from a DBObjectSet already in memory
* @param $oSet DBObjectSet
@@ -73,7 +78,14 @@ class DisplayBlock
$aKeys[] = $oObject->GetKey();
}
$oSet->Rewind();
$oDummyFilter->AddCondition('id', $aKeys, 'IN');
if (count($aKeys) > 0)
{
$oDummyFilter->AddCondition('id', $aKeys, 'IN');
}
else
{
$oDummyFilter->AddCondition('id', 0, '=');
}
$oBlock = new DisplayBlock($oDummyFilter, $sStyle, false, $aParams); // DisplayBlocks built this way are synchronous
return $oBlock;
}
@@ -192,6 +204,11 @@ class DisplayBlock
$bAutoReload = false;
if (isset($aExtraParams['auto_reload']))
{
if ($aExtraParams['auto_reload'] === true)
{
// Note: does not work in the switch (case true) because a positive number evaluates to true!!!
$aExtraParams['auto_reload'] = 'standard';
}
switch($aExtraParams['auto_reload'])
{
case 'fast':
@@ -201,16 +218,15 @@ class DisplayBlock
case 'standard':
case 'true':
case true:
$bAutoReload = true;
$iReloadInterval = MetaModel::GetConfig()->GetStandardReloadInterval()*1000;
break;
default:
if (is_numeric($aExtraParams['auto_reload']))
if (is_numeric($aExtraParams['auto_reload']) && ($aExtraParams['auto_reload'] > 0))
{
$bAutoReload = true;
$iReloadInterval = $aExtraParams['auto_reload']*1000;
$iReloadInterval = max(5, $aExtraParams['auto_reload'])*1000;
}
else
{
@@ -245,7 +261,7 @@ class DisplayBlock
);
');
}
if ($bAutoReload)
if (($bAutoReload) && ($this->m_sStyle != 'search')) // Search form do NOT auto-reload
{
$oPage->add_script('setInterval("ReloadBlock(\''.$sId.'\', \''.$this->m_sStyle.'\', \''.$sFilter.'\', \"'.$sExtraParams.'\")", '.$iReloadInterval.');');
}
@@ -390,7 +406,7 @@ class DisplayBlock
$aGroupBy = array();
$aGroupBy['grouped_by_1'] = $oGroupByExp;
$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy);
$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy, true);
$aRes = CMDBSource::QueryToArray($sSql);
$aGroupBy = array();
@@ -768,6 +784,7 @@ class DisplayBlock
$sChecked = '';
}
/*
$sCSVData = cmdbAbstractObject::GetSetAsCSV($this->m_oSet, array('fields_advanced' => $bAdvancedMode));
$sCharset = MetaModel::GetConfig()->Get('csv_file_default_charset');
if ($sCharset == 'UTF-8')
@@ -796,6 +813,8 @@ class DisplayBlock
$sCharsetNotice = '';
}
*/
$sCharsetNotice = false;
$sHtml .= "<div>";
$sHtml .= '<table style="width:100%" class="transparent">';
$sHtml .= '<tr>';
@@ -811,9 +830,10 @@ class DisplayBlock
}
$sHtml .= "</div>";
$sHtml .= "<textarea style=\"width:95%;height:98%\">\n";
$sHtml .= htmlentities($sCSVData, ENT_QUOTES, 'UTF-8');
$sHtml .= "<div id=\"csv_content_loading\"><div style=\"width: 250px; height: 20px; background: url(../setup/orange-progress.gif); border: 1px #999 solid; margin-left:auto; margin-right: auto; text-align: center;\">".Dict::S('UI:Loading')."</div></div><textarea id=\"csv_content\" style=\"display:none;\">\n";
//$sHtml .= htmlentities($sCSVData, ENT_QUOTES, 'UTF-8');
$sHtml .= "</textarea>\n";
$oPage->add_ready_script("$.post('$sDownloadLink', {}, function(data) { $('#csv_content').html(data); $('#csv_content_loading').hide(); $('#csv_content').show();} );");
break;
case 'modify':
@@ -888,7 +908,7 @@ EOF
$aGroupBy = array();
$aGroupBy['grouped_by_1'] = $oGroupByExp;
$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy);
$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy, true);
$aRes = CMDBSource::QueryToArray($sSql);
$aGroupBy = array();
@@ -963,7 +983,7 @@ EOF
$aGroupBy = array();
$aGroupBy['grouped_by_1'] = $oGroupByExp;
$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy);
$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy, true);
$aRes = CMDBSource::QueryToArray($sSql);
$aGroupBy = array();
@@ -1044,7 +1064,8 @@ EOF
$aGroupBy = array();
$aGroupBy['grouped_by_1'] = $oGroupByExp;
$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy);
$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy, true);
$aRes = CMDBSource::QueryToArray($sSql);
$aGroupBy = array();
@@ -1088,6 +1109,7 @@ EOF
$oTitle = new title($sTitle);
$oChart->set_title( $oTitle );
$oTitle->set_style("{font-size: 16px; font-family: Tahoma; font-weight: bold; text-align: center;}");
}
$oChart->set_bg_colour('#FFFFFF');
$oChart->add_element( $oChartElement );
@@ -1188,10 +1210,35 @@ EOF
*/
class HistoryBlock extends DisplayBlock
{
protected $iLimitCount;
protected $iLimitStart;
public function __construct(DBObjectSearch $oFilter, $sStyle = 'list', $bAsynchronous = false, $aParams = array(), $oSet = null)
{
parent::__construct($oFilter, $sStyle, $bAsynchronous, $aParams, $oSet);
$this->iLimitStart = 0;
$this->iLimitCount = 0;
}
public function SetLimit($iCount, $iStart = 0)
{
$this->iLimitStart = $iStart;
$this->iLimitCount = $iCount;
}
public function GetRenderContent(WebPage $oPage, $aExtraParams = array(), $sId)
{
$sHtml = '';
$bTruncated = false;
$oSet = new CMDBObjectSet($this->m_oFilter, array('date'=>false));
if (($this->iLimitStart > 0) || ($this->iLimitCount > 0))
{
$oSet->SetLimit($this->iLimitCount, $this->iLimitStart);
if (($this->iLimitCount - $this->iLimitStart) < $oSet->Count())
{
$bTruncated = true;
}
}
$sHtml .= "<!-- filter: ".($this->m_oFilter->ToOQL())."-->\n";
switch($this->m_sStyle)
{
@@ -1217,7 +1264,21 @@ class HistoryBlock extends DisplayBlock
case 'table':
default:
$sHtml .= $this->GetHistoryTable($oPage, $oSet);
if ($bTruncated)
{
$sFilter = $this->m_oFilter->serialize();
$sHtml .= '<div id="history_container"><p>';
$sHtml .= Dict::Format('UI:TruncatedResults', $this->iLimitCount, $oSet->Count());
$sHtml .= ' ';
$sHtml .= '<a href="#" onclick="DisplayHistory(\'#history_container\', \''.$sFilter.'\', 0, 0); return false;">'.Dict::S('UI:DisplayAll').'</a>';
$sHtml .= $this->GetHistoryTable($oPage, $oSet);
$sHtml .= '</p></div>';
$oPage->add_ready_script("$('#{$sId} table.listResults tr:last td').addClass('truncated');");
}
else
{
$sHtml .= $this->GetHistoryTable($oPage, $oSet);
}
}
return $sHtml;
@@ -1289,6 +1350,7 @@ class MenuBlock extends DisplayBlock
$sContext = '&'.$sContext;
}
$sClass = $this->m_oFilter->GetClass();
$oReflectionClass = new ReflectionClass($sClass);
$oSet = new CMDBObjectSet($this->m_oFilter);
$sFilter = $this->m_oFilter->serialize();
$sFilterDesc = $this->m_oFilter->ToOql(true);
@@ -1308,7 +1370,7 @@ class MenuBlock extends DisplayBlock
$sDefault.= "&default[$sKey]=$sValue";
}
}
$bIsCreationAllowed = (UserRights::IsActionAllowed($sClass, UR_ACTION_CREATE) == UR_ALLOWED_YES);
$bIsCreationAllowed = (UserRights::IsActionAllowed($sClass, UR_ACTION_CREATE) == UR_ALLOWED_YES) && ($oReflectionClass->IsSubclassOf('cmdbAbstractObject'));
switch($oSet->Count())
{
case 0:
@@ -1319,10 +1381,8 @@ class MenuBlock extends DisplayBlock
case 1:
$oObj = $oSet->Fetch();
$id = $oObj->GetKey();
$bIsModifyAllowed = (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $oSet) == UR_ALLOWED_YES);
$bIsModifyAllowed = (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $oSet) == UR_ALLOWED_YES) && ($oReflectionClass->IsSubclassOf('cmdbAbstractObject'));
$bIsDeleteAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, $oSet);
$bIsBulkModifyAllowed = (!MetaModel::IsAbstract($sClass)) && UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, $oSet);
$bIsBulkDeleteAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, $oSet);
// Just one object in the set, possible actions are "new / clone / modify and delete"
if (!isset($aExtraParams['link_attr']))
{
@@ -1388,8 +1448,8 @@ class MenuBlock extends DisplayBlock
default:
// Check rights
// New / Modify
$bIsModifyAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $oSet);
$bIsBulkModifyAllowed = (!MetaModel::IsAbstract($sClass)) && UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, $oSet);
$bIsModifyAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $oSet) && ($oReflectionClass->IsSubclassOf('cmdbAbstractObject'));
$bIsBulkModifyAllowed = (!MetaModel::IsAbstract($sClass)) && UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, $oSet) && ($oReflectionClass->IsSubclassOf('cmdbAbstractObject'));
$bIsBulkDeleteAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, $oSet);
if (isset($aExtraParams['link_attr']))
{
@@ -1417,17 +1477,21 @@ class MenuBlock extends DisplayBlock
if ((count($aStates) > 0) && (($iLimit == 0) || ($oSet->Count() < $iLimit)))
{
// Life cycle actions may be available... if all objects are in the same state
$oSet->Rewind();
$aStates = array();
while($oObj = $oSet->Fetch())
//
// Group by <state>
$oGroupByExp = new FieldExpression(MetaModel::GetStateAttributeCode($sClass), $this->m_oFilter->GetClassAlias());
$aGroupBy = array('__state__' => $oGroupByExp);
$aQueryParams = array();
if (isset($aExtraParams['query_params']))
{
$aStates[$oObj->GetState()] = $oObj->GetState();
$aQueryParams = $aExtraParams['query_params'];
}
$oSet->Rewind();
if (count($aStates) == 1)
$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy);
$aRes = CMDBSource::QueryToArray($sSql);
if (count($aRes) == 1)
{
// All objects are in the same state...
$sState = array_pop($aStates);
$sState = $aRes[0]['__state__'];
$aTransitions = Metamodel::EnumTransitions($sClass, $sState);
if (count($aTransitions))
{
@@ -1435,8 +1499,11 @@ class MenuBlock extends DisplayBlock
$aStimuli = Metamodel::EnumStimuli($sClass);
foreach($aTransitions as $sStimulusCode => $aTransitionDef)
{
$oChecker = new StimulusChecker($this->m_oFilter, $sState, $sStimulusCode);
$iActionAllowed = (get_class($aStimuli[$sStimulusCode]) == 'StimulusUserAction') ? $oChecker->IsAllowed() : UR_ALLOWED_NO;
$oSet->Rewind();
// As soon as the user rights implementation will browse the object set,
// then we might consider using OptimizeColumnLoad() here
$iActionAllowed = UserRights::IsStimulusAllowed($sClass, $sStimulusCode, $oSet);
$iActionAllowed = (get_class($aStimuli[$sStimulusCode]) == 'StimulusUserAction') ? $iActionAllowed : UR_ALLOWED_NO;
switch($iActionAllowed)
{
case UR_ALLOWED_YES:
@@ -1559,114 +1626,3 @@ class MenuBlock extends DisplayBlock
}
}
}
/**
* Some dummy menus for testing
*/
class ExtraMenus implements iPopupMenuExtension
{
/*
const MENU_OBJLIST_ACTIONS = 1; // $param is a DBObjectSet containing the list of objects
const MENU_OBJLIST_TOOLKIT = 2; // $param is a DBObjectSet containing the list of objects
const MENU_OBJDETAILS_ACTIONS = 3; // $param is a DBObject instance: the object currently displayed
const MENU_DASHBOARD_ACTIONS = 4; // $param is a Dashboard instance: the dashboard currently displayed
const MENU_USER_ACTIONS = 5; // $param is a null ??
*/
/**
* Get the list of items to be added to a menu. The items will be inserted in the menu in the order of the returned array
* @param int $iMenuId The identifier of the type of menu, as listed by the constants MENU_xxx above
* @param mixed $param Depends on $iMenuId see the constants define above
* @return Array An array of ApplicationPopupMenuItem or an empty array if no action is to be added to the menu
*/
public static function EnumItems($iMenuId, $param)
{
switch($iMenuId)
{
/*
case iPopupMenuExtension::MENU_OBJLIST_ACTIONS:
// $param is a DBObjectSet
$aResult = array(
new JSPopupMenuItem('Test::Item1', 'List Test 1', "alert('Test 1')"),
new JSPopupMenuItem('Test::Item2', 'List Test 2', "alert('Test 2')"),
);
break;
$this->AddMenuSeparator($aActions);
$sUrl = utils::GetAbsoluteUrlAppRoot();
$aActions['UI:Menu:EMail'] = array ('label' => Dict::S('UI:Menu:EMail'), 'url' => "mailto:?subject=$sFilterDesc&body=".urlencode("{$sUrl}pages/$sUIPage?operation=search&filter=".urlencode($sFilter)."{$sContext}"));
$aActions['UI:Menu:CSVExport'] = array ('label' => Dict::S('UI:Menu:CSVExport'), 'url' => "{$sRootUrl}pages/$sUIPage?operation=search&filter=".urlencode($sFilter)."&format=csv{$sContext}");
$sOQL = addslashes($sFilterDesc);
$aActions['UI:Menu:AddToDashboard'] = array ('label' => Dict::S('UI:Menu:AddToDashboard'), 'url' => "#", 'onclick' => "return DashletCreationDlg('$sOQL')");
*/
case iPopupMenuExtension::MENU_OBJLIST_TOOLKIT:
// $param is a DBObjectSet
$oAppContext = new ApplicationContext();
$sContext = $oAppContext->GetForLink();
$sUIPage = cmdbAbstractObject::ComputeStandardUIPage($param->GetFilter()->GetClass());
$sOQL = addslashes($param->GetFilter()->ToOQL(true));
$sFilter = urlencode($param->GetFilter()->serialize());
$sUrl = utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".$sFilter."&{$sContext}";
$aResult = array(
new SeparatorPopupMenuItem(),
// Static menus: Email this page, CSV Export & Add to Dashboard
new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?body=".urlencode($sUrl)),
new URLPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), $sUrl."&format=csv"),
new JSPopupMenuItem('UI:Menu:AddToDashboard', Dict::S('UI:Menu:AddToDashboard'), "DashletCreationDlg('$sOQL')"),
new JSPopupMenuItem('UI:Menu:ShortcutList', Dict::S('UI:Menu:ShortcutList'), "ShortcutListDlg('$sOQL', '$sContext')"),
);
break;
case iPopupMenuExtension::MENU_OBJDETAILS_ACTIONS:
// $param is a DBObject
$oObj = $param;
$oFilter = DBobjectSearch::FromOQL("SELECT ".get_class($oObj)." WHERE id=".$oObj->GetKey());
$sFilter = $oFilter->serialize();
$sUrl = ApplicationContext::MakeObjectUrl(get_class($oObj), $oObj->GetKey());
$sUIPage = cmdbAbstractObject::ComputeStandardUIPage(get_class($oObj));
$oAppContext = new ApplicationContext();
$sContext = $oAppContext->GetForLink();
$aResult = array(
new SeparatorPopupMenuItem(),
// Static menus: Email this page & CSV Export
new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?subject=".urlencode($oObj->GetRawName())."&body=".urlencode($sUrl)),
new URLPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".urlencode($sFilter)."&format=csv&{$sContext}"),
);
break;
case iPopupMenuExtension::MENU_DASHBOARD_ACTIONS:
// $param is a Dashboard
$oAppContext = new ApplicationContext();
$aParams = $oAppContext->GetAsHash();
$sMenuId = ApplicationMenu::GetActiveNodeId();
$sDlgTitle = addslashes(Dict::S('UI:ImportDashboardTitle'));
$sDlgText = addslashes(Dict::S('UI:ImportDashboardText'));
$sCloseBtn = addslashes(Dict::S('UI:Button:Cancel'));
$aResult = array(
new SeparatorPopupMenuItem(),
new URLPopupMenuItem('UI:ExportDashboard', Dict::S('UI:ExportDashBoard'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=export_dashboard&id='.$sMenuId),
new JSPopupMenuItem('UI:ImportDashboard', Dict::S('UI:ImportDashBoard'), "UploadDashboard({dashboard_id: '$sMenuId', title: '$sDlgTitle', text: '$sDlgText', close_btn: '$sCloseBtn' })"),
);
break;
/*
case iPopupMenuExtension::MENU_USER_ACTIONS:
// $param is null ??
$aResult = array(
new SeparatorPopupMenuItem(),
new JSPopupMenuItem('Test::Item1', 'Reset preferences...', "alert('Test 1')"),
new JSPopupMenuItem('Test::Item2', 'Do Something Stupid', "alert('Hey Dude !')"),
);
break;
*/
default:
// Unknown type of menu, do nothing
$aResult = array();
}
return $aResult;
}
}

View File

@@ -36,6 +36,9 @@ class DesignerForm
protected $aSubmitParams;
protected $sSubmitTo;
protected $bReadOnly;
protected $sHierarchyPath; // Needed to manage the visibility of nested subform
protected $sHierarchyParent; // Needed to manage the visibility of nested subform
protected $bDisplayed;
public function __construct()
{
@@ -48,7 +51,10 @@ class DesignerForm
$this->sFormId = 'form_'.rand();
$this->oParentForm = null;
$this->bReadOnly = false;
$this->sHierarchyPath = '';
$this->sHierarchyParent = '';
$this->StartFieldSet($this->sCurrentFieldSet);
$this->bDisplayed = true;
}
public function AddField(DesignerFormField $oField)
@@ -72,7 +78,6 @@ class DesignerForm
public function Render($oP, $bReturnHTML = false)
{
$sReturn = '';
if ($this->oParentForm == null)
{
$sFormId = $this->sFormId;
@@ -80,6 +85,7 @@ class DesignerForm
}
else
{
$sReturn = '';
$sFormId = $this->oParentForm->sFormId;
}
$sHiddenFields = '';
@@ -147,6 +153,38 @@ class DesignerForm
$this->aSubmitParams = $oParentForm->aSubmitParams;
}
/**
* Helper to handle subforms hide/show
*/
public function SetHierarchyPath($sHierarchy)
{
$this->sHierarchyPath = $sHierarchy;
}
/**
* Helper to handle subforms hide/show
*/
public function GetHierarchyPath()
{
return $this->sHierarchyPath;
}
/**
* Helper to handle subforms hide/show
*/
public function SetHierarchyParent($sHierarchy)
{
$this->sHierarchyParent = $sHierarchy;
}
/**
* Helper to handle subforms hide/show
*/
public function GetHierarchyParent()
{
return $this->sHierarchyParent;
}
public function RenderAsPropertySheet($oP, $bReturnHTML = false, $sNotifyParentSelector = null)
{
@@ -250,12 +288,17 @@ EOF
$oP->add($sReturn);
}
}
public function RenderAsDialog($oPage, $sDialogId, $sDialogTitle, $iDialogWidth, $sOkButtonLabel)
public function RenderAsDialog($oPage, $sDialogId, $sDialogTitle, $iDialogWidth, $sOkButtonLabel, $sIntroduction = null)
{
$sDialogTitle = addslashes($sDialogTitle);
$sOkButtonLabel = addslashes($sOkButtonLabel);
$sCancelButtonLabel = 'Cancel'; //TODO: localize
$sCancelButtonLabel = Dict::S('UI:Button:Cancel');
$oPage->add("<div id=\"$sDialogId\">");
if ($sIntroduction != null)
{
$oPage->add('<div class="ui-dialog-header">'.$sIntroduction.'</div>');
}
$this->Render($oPage);
$oPage->add('</div>');
@@ -268,7 +311,7 @@ $('#$sDialogId').dialog({
title: '$sDialogTitle',
buttons: [
{ text: "$sOkButtonLabel", click: function() {
var oForm = $(this).parents('.ui-dialog :first').find('form');
var oForm = $(this).closest('.ui-dialog').find('form');
oForm.submit();
} },
{ text: "$sCancelButtonLabel", click: function() { KillAllMenus(); $(this).dialog( "close" ); $(this).remove(); } },
@@ -343,6 +386,28 @@ EOF
$this->oParentForm = $oParentForm;
}
public function GetParentForm()
{
return $this->oParentForm;
}
public function SetDisplayed($bDisplayed)
{
$this->bDisplayed = $bDisplayed;
}
public function IsDisplayed()
{
if ($this->oParentForm == null)
{
return $this->bDisplayed;
}
else
{
return ($this->bDisplayed && $this->oParentForm->IsDisplayed());
}
}
public function AddScript($sScript)
{
$this->sScript .= $sScript;
@@ -489,6 +554,8 @@ class DesignerFormField
protected $bMandatory;
protected $bReadOnly;
protected $bAutoApply;
protected $aCSSClasses;
protected $bDisplayed;
public function __construct($sCode, $sLabel, $defaultValue)
{
@@ -498,6 +565,8 @@ class DesignerFormField
$this->bMandatory = false;
$this->bReadOnly = false;
$this->bAutoApply = false;
$this->aCSSClasses = array();
$this->bDisplayed = true;
}
public function GetCode()
@@ -536,6 +605,16 @@ class DesignerFormField
return $this->bAutoApply;
}
public function SetDisplayed($bDisplayed)
{
$this->bDisplayed = $bDisplayed;
}
public function IsDisplayed()
{
return $this->bDisplayed;
}
public function Render(WebPage $oP, $sFormId, $sRenderMode='dialog')
{
$sId = $this->oForm->GetFieldId($this->sCode);
@@ -574,6 +653,11 @@ class DesignerFormField
{
return true;
}
public function AddCSSClass($sCSSClass)
{
$this->aCSSClasses[] = $sCSSClass;
}
}
class DesignerLabelField extends DesignerFormField
@@ -606,34 +690,75 @@ class DesignerLabelField extends DesignerFormField
class DesignerTextField extends DesignerFormField
{
protected $sValidationPattern;
protected $aForbiddenValues;
protected $sExplainForbiddenValues;
public function __construct($sCode, $sLabel = '', $defaultValue = '')
{
parent::__construct($sCode, $sLabel, $defaultValue);
$this->sValidationPattern = '';
$this->aForbiddenValues = null;
$this->sExplainForbiddenValues = null;
}
public function SetValidationPattern($sValidationPattern)
{
$this->sValidationPattern = $sValidationPattern;
}
public function SetForbiddenValues($aValues, $sExplain)
{
$this->aForbiddenValues = $aValues;
$iDefaultKey = array_search($this->defaultValue, $this->aForbiddenValues);
if ($iDefaultKey !== false)
{
// The default (current) value is always allowed...
unset($this->aForbiddenValues[$iDefaultKey]);
}
$this->sExplainForbiddenValues = $sExplain;
}
public function Render(WebPage $oP, $sFormId, $sRenderMode='dialog')
{
$sId = $this->oForm->GetFieldId($this->sCode);
$sName = $this->oForm->GetFieldName($this->sCode);
$sPattern = addslashes($this->sValidationPattern);
$sMandatory = $this->bMandatory ? 'true' : 'false';
$sReadOnly = $this->IsReadOnly() ? 'readonly' : '';
$oP->add_ready_script(
if ($this->IsReadOnly())
{
$sHtmlValue = "<span>".htmlentities($this->defaultValue, ENT_QUOTES, 'UTF-8')."<input type=\"hidden\" id=\"$sId\" name=\"$sName\" value=\"".htmlentities($this->defaultValue, ENT_QUOTES, 'UTF-8')."\"/></span>";
}
else
{
$sPattern = addslashes($this->sValidationPattern);
if (is_array($this->aForbiddenValues))
{
$sForbiddenValues = json_encode($this->aForbiddenValues);
$sExplainForbiddenValues = addslashes($this->sExplainForbiddenValues);
}
else
{
$sForbiddenValues = 'null';
$sExplainForbiddenValues = 'null';
}
$sMandatory = $this->bMandatory ? 'true' : 'false';
$oP->add_ready_script(
<<<EOF
$('#$sId').bind('change keyup validate', function() { ValidateWithPattern('$sId', $sMandatory, '$sPattern', '$sFormId'); } );
$('#$sId').bind('change keyup validate', function() { ValidateWithPattern('$sId', $sMandatory, '$sPattern', '$sFormId', $sForbiddenValues, '$sExplainForbiddenValues'); } );
{
var myTimer = null;
$('#$sId').bind('keyup', function() { clearTimeout(myTimer); myTimer = setTimeout(function() { $('#$sId').trigger('change', {} ); }, 100); });
}
EOF
);
return array('label' => $this->sLabel, 'value' => "<input type=\"text\" id=\"$sId\" $sReadOnly name=\"$sName\" value=\"".htmlentities($this->defaultValue, ENT_QUOTES, 'UTF-8')."\">");
);
$sCSSClasses = '';
if (count($this->aCSSClasses) > 0)
{
$sCSSClasses = 'class="'.implode(' ', $this->aCSSClasses).'"';
}
$sHtmlValue = "<input type=\"text\" $sCSSClasses id=\"$sId\" name=\"$sName\" value=\"".htmlentities($this->defaultValue, ENT_QUOTES, 'UTF-8')."\">";
}
return array('label' => $this->sLabel, 'value' => $sHtmlValue);
}
public function ReadParam(&$aValues)
@@ -644,6 +769,11 @@ EOF
{
$aValues[$this->sCode] = $this->defaultValue;
}
else if(($this->aForbiddenValues != null) && in_array($aValues[$this->sCode], $this->aForbiddenValues))
{
// Reject the value...
$aValues[$this->sCode] = $this->defaultValue;
}
}
}
@@ -654,18 +784,33 @@ class DesignerLongTextField extends DesignerTextField
$sId = $this->oForm->GetFieldId($this->sCode);
$sName = $this->oForm->GetFieldName($this->sCode);
$sPattern = addslashes($this->sValidationPattern);
if (is_array($this->aForbiddenValues))
{
$sForbiddenValues = json_encode($this->aForbiddenValues);
$sExplainForbiddenValues = addslashes($this->sExplainForbiddenValues);
}
else
{
$sForbiddenValues = 'null';
$sExplainForbiddenValues = 'null';
}
$sMandatory = $this->bMandatory ? 'true' : 'false';
$sReadOnly = $this->IsReadOnly() ? 'readonly' : '';
$oP->add_ready_script(
<<<EOF
$('#$sId').bind('change keyup validate', function() { ValidateWithPattern('$sId', $sMandatory, '$sPattern', '$sFormId'); } );
$('#$sId').bind('change keyup validate', function() { ValidateWithPattern('$sId', $sMandatory, '$sPattern', '$sFormId', $sForbiddenValues, '$sExplainForbiddenValues'); } );
{
var myTimer = null;
$('#$sId').bind('keyup', function() { clearTimeout(myTimer); myTimer = setTimeout(function() { $('#$sId').trigger('change', {} ); }, 100); });
}
EOF
);
return array('label' => $this->sLabel, 'value' => "<textarea id=\"$sId\" $sReadOnly name=\"$sName\">".htmlentities($this->defaultValue, ENT_QUOTES, 'UTF-8')."</textarea>");
$sCSSClasses = '';
if (count($this->aCSSClasses) > 0)
{
$sCSSClasses = 'class="'.implode(' ', $this->aCSSClasses).'"';
}
return array('label' => $this->sLabel, 'value' => "<textarea $sCSSClasses id=\"$sId\" $sReadOnly name=\"$sName\">".htmlentities($this->defaultValue, ENT_QUOTES, 'UTF-8')."</textarea>");
}
}
@@ -709,39 +854,72 @@ class DesignerComboField extends DesignerFormField
$sChecked = $this->defaultValue ? 'checked' : '';
$sMandatory = $this->bMandatory ? 'true' : 'false';
$sReadOnly = $this->IsReadOnly() ? 'disabled="disabled"' : '';
if ($this->bMultipleSelection)
$sCSSClasses = '';
if (count($this->aCSSClasses) > 0)
{
$sHtml = "<select multiple size=\"8\"id=\"$sId\" name=\"$sName\" $sReadOnly>";
$sCSSClasses = 'class="'.implode(' ', $this->aCSSClasses).'"';
}
if ($this->IsReadOnly())
{
$aSelected = array();
$aHiddenValues = array();
foreach($this->aAllowedValues as $sKey => $sDisplayValue)
{
if ($this->bMultipleSelection)
{
if(in_array($sKey, $this->defaultValue))
{
$aSelected[] = $sDisplayValue;
$aHiddenValues[] = "<input type=\"hidden\" name=\"{$sName}[]\" value=\"".htmlentities($sKey, ENT_QUOTES, 'UTF-8')."\"/>";
}
}
else
{
if ($sKey == $this->defaultValue)
{
$aSelected[] = $sDisplayValue;
$aHiddenValues[] = "<input type=\"hidden\" id=\"$sId\" name=\"$sName\" value=\"".htmlentities($sKey, ENT_QUOTES, 'UTF-8')."\"/>";
}
}
}
$sHtml = "<span $sCSSClasses>".htmlentities(implode(', ', $aSelected), ENT_QUOTES, 'UTF-8').implode($aHiddenValues)."</span>";
}
else
{
$sHtml = "<select id=\"$sId\" name=\"$sName\" $sReadOnly>";
$sHtml .= "<option value=\"\">".Dict::S('UI:SelectOne')."</option>";
}
foreach($this->aAllowedValues as $sKey => $sDisplayValue)
{
if ($this->bMultipleSelection)
{
$sSelected = in_array($sKey, $this->defaultValue) ? 'selected' : '';
$sHtml = "<select $sCSSClasses multiple size=\"8\"id=\"$sId\" name=\"$sName\">";
}
else
{
$sSelected = ($sKey == $this->defaultValue) ? 'selected' : '';
$sHtml = "<select $sCSSClasses id=\"$sId\" name=\"$sName\">";
$sHtml .= "<option value=\"\">".Dict::S('UI:SelectOne')."</option>";
}
// Quick and dirty: display the menu parents as a tree
$sHtmlValue = str_replace(' ', '&nbsp;', htmlentities($sDisplayValue, ENT_QUOTES, 'UTF-8'));
$sHtml .= "<option value=\"".htmlentities($sKey, ENT_QUOTES, 'UTF-8')."\" $sSelected>$sHtmlValue</option>";
}
$sHtml .= "</select>";
if ($this->bOtherChoices)
{
$sHtml .= '<br/><input type="checkbox" id="other_chk_'.$sId.'"><label for="other_chk_'.$sId.'">&nbsp;Other:</label>&nbsp;<input type="text" id="other_'.$sId.'" name="other_'.$sName.'" size="30"/>';
}
$oP->add_ready_script(
foreach($this->aAllowedValues as $sKey => $sDisplayValue)
{
if ($this->bMultipleSelection)
{
$sSelected = in_array($sKey, $this->defaultValue) ? 'selected' : '';
}
else
{
$sSelected = ($sKey == $this->defaultValue) ? 'selected' : '';
}
// Quick and dirty: display the menu parents as a tree
$sHtmlValue = str_replace(' ', '&nbsp;', htmlentities($sDisplayValue, ENT_QUOTES, 'UTF-8'));
$sHtml .= "<option value=\"".htmlentities($sKey, ENT_QUOTES, 'UTF-8')."\" $sSelected>$sHtmlValue</option>";
}
$sHtml .= "</select>";
if ($this->bOtherChoices)
{
$sHtml .= '<br/><input type="checkbox" id="other_chk_'.$sId.'"><label for="other_chk_'.$sId.'">&nbsp;Other:</label>&nbsp;<input type="text" id="other_'.$sId.'" name="other_'.$sName.'" size="30"/>';
}
$oP->add_ready_script(
<<<EOF
$('#$sId').bind('change validate', function() { ValidateWithPattern('$sId', $sMandatory, '', '$sFormId'); } );
$('#$sId').bind('change validate', function() { ValidateWithPattern('$sId', $sMandatory, '', '$sFormId', null, null); } );
EOF
);
);
}
return array('label' => $this->sLabel, 'value' => $sHtml);
}
@@ -768,9 +946,21 @@ class DesignerBooleanField extends DesignerFormField
$sId = $this->oForm->GetFieldId($this->sCode);
$sName = $this->oForm->GetFieldName($this->sCode);
$sChecked = $this->defaultValue ? 'checked' : '';
$sReadOnly = $this->IsReadOnly() ? 'disabled' : ''; // readonly does not work as expected on checkboxes:
// readonly prevents the user from changing the input's value not its state (checked/unchecked)
return array('label' => $this->sLabel, 'value' => "<input type=\"checkbox\" $sChecked $sReadOnly id=\"$sId\" name=\"$sName\" value=\"true\">");
if ($this->IsReadOnly())
{
$sLabel = $this->defaultValue ? Dict::S('UI:UserManagement:ActionAllowed:Yes') : Dict::S('UI:UserManagement:ActionAllowed:No'); //TODO use our own yes/no translations
$sHtmlValue = "<span>".htmlentities($sLabel)."<input type=\"hidden\" id=\"$sId\" name=\"$sName\" value=\"".htmlentities($this->defaultValue, ENT_QUOTES, 'UTF-8')."\"/></span>";
}
else
{
$sCSSClasses = '';
if (count($this->aCSSClasses) > 0)
{
$sCSSClasses = 'class="'.implode(' ', $this->aCSSClasses).'"';
}
$sHtmlValue = "<input $sCSSClasses type=\"checkbox\" $sChecked id=\"$sId\" name=\"$sName\" value=\"true\">";
}
return array('label' => $this->sLabel, 'value' => $sHtmlValue);
}
public function ReadParam(&$aValues)
@@ -798,8 +988,8 @@ class DesignerBooleanField extends DesignerFormField
{
$sValue = utils::ReadParam($this->oForm->GetParamName($this->sCode), 'false', false, 'raw_data');
}
$aValues[$this->sCode] = ($sValue == 'true');
}
$aValues[$this->sCode] = ($sValue == 'true');
}
}
@@ -828,19 +1018,26 @@ class DesignerHiddenField extends DesignerFormField
class DesignerIconSelectionField extends DesignerFormField
{
protected $sUploadUrl;
protected $aAllowedValues;
public function __construct($sCode, $sLabel = '', $defaultValue = '')
{
parent::__construct($sCode, $sLabel, $defaultValue);
$this->bAutoApply = true;
$this->sUploadUrl = null;
}
public function SetAllowedValues($aAllowedValues)
{
$this->aAllowedValues = $aAllowedValues;
}
public function EnableUpload($sIconUploadUrl)
{
$this->sUploadUrl = $sIconUploadUrl;
}
public function Render(WebPage $oP, $sFormId, $sRenderMode='dialog')
{
$sId = $this->oForm->GetFieldId($this->sCode);
@@ -855,11 +1052,12 @@ class DesignerIconSelectionField extends DesignerFormField
}
}
$sJSItems = json_encode($this->aAllowedValues);
$sPostUploadTo = ($this->sUploadUrl == null) ? 'null' : "'{$this->sUploadUrl}'";
if (!$this->IsReadOnly())
{
$oP->add_ready_script(
<<<EOF
$('#$sId').icon_select({current_idx: $idx, items: $sJSItems});
$('#$sId').icon_select({current_idx: $idx, items: $sJSItems, post_upload_to: $sPostUploadTo});
EOF
);
}
@@ -868,6 +1066,71 @@ EOF
}
}
class RunTimeIconSelectionField extends DesignerIconSelectionField
{
public function __construct($sCode, $sLabel = '', $defaultValue = '')
{
parent::__construct($sCode, $sLabel, $defaultValue);
$aAllIcons = self::FindIconsOnDisk(APPROOT.'env-'.utils::GetCurrentEnvironment());
ksort($aAllIcons);
$aValues = array();
foreach($aAllIcons as $sFilePath)
{
$aValues[] = array('value' => $sFilePath, 'label' => basename($sFilePath), 'icon' => utils::GetAbsoluteUrlModulesRoot().$sFilePath);
}
$this->SetAllowedValues($aValues);
}
static protected function FindIconsOnDisk($sBaseDir, $sDir = '')
{
$aResult = array();
// Populate automatically the list of icon files
if ($hDir = @opendir($sBaseDir.'/'.$sDir))
{
while (($sFile = readdir($hDir)) !== false)
{
$aMatches = array();
if (($sFile != '.') && ($sFile != '..') && ($sFile != 'lifecycle') && is_dir($sBaseDir.'/'.$sDir.'/'.$sFile))
{
$sDirSubPath = ($sDir == '') ? $sFile : $sDir.'/'.$sFile;
$aResult = array_merge($aResult, self::FindIconsOnDisk($sBaseDir, $sDirSubPath));
}
if (preg_match("/\.(png|jpg|jpeg|gif)$/i", $sFile, $aMatches)) // png, jp(e)g and gif are considered valid
{
$aResult[$sFile.'_'.$sDir] = $sDir.'/'.$sFile;
}
}
closedir($hDir);
}
return $aResult;
}
public function ValueFromDOMNode($oDOMNode)
{
return $oDOMNode->textContent;
}
public function ValueToDOMNode($oDOMNode, $value)
{
$oTextNode = $oDOMNode->ownerDocument->createTextNode($value);
$oDOMNode->appendChild($oTextNode);
}
public function MakeFileUrl($value)
{
return utils::GetAbsoluteUrlModulesRoot().$value;
}
public function GetDefaultValue($sClass = 'Contact')
{
$sIconPath = MetaModel::GetClassIcon($sClass, false);
$sIcon = str_replace(utils::GetAbsoluteUrlModulesRoot(), '', $sIconPath);
return $sIcon;
}
}
class DesignerSortableField extends DesignerFormField
{
protected $aAllowedValues;
@@ -888,31 +1151,16 @@ class DesignerSortableField extends DesignerFormField
$bOpen = false;
$sId = $this->oForm->GetFieldId($this->sCode);
$sName = $this->oForm->GetFieldName($this->sCode);
$sHtml = "<span class=\"sort_$sId fieldslist\" id=\"sortable_$sId\">";
foreach($this->defaultValue as $sValue)
{
$sHtml .= "<span class=\"movable_attr\">$sValue</span>";
}
$sHtml .="</span>";
$sIconClass = $bOpen ? 'ui-icon-circle-triangle-s' : 'ui-icon-circle-triangle-e';
$sStyle = $bOpen ? '' : 'style="display:none"';
$sHtml .= "<div class=\"fieldspicker\"><table><tr><td><span id=\"collapse_$sId\" class=\"ui-icon $sIconClass\" style=\"opacity: 0.5\"></span>Fields</td></tr><tr><td><div $sStyle id=\"fieldsbasket_$sId\" class=\"sort_$sId fieldsbasket\">";
foreach($this->aAllowedValues as $sKey => $sDisplayValue)
{
$sHtml .= "<span class=\"movable_attr\">$sDisplayValue</span>";
}
$sHtml .="</div></td></tr>";
$sHtml .="<tr id=\"trash_icon_$sId\" $sStyle><td><span class=\"ui-icon ui-icon-trash\" style=\"opacity: 0.5\"></span>Trash</td></tr><tr id=\"trash_$sId\" $sStyle><td><div id=\"recycle_$sId\" class=\"sort_$sId fieldstrash\"></div></div></td></tr></table></div>";
$sReadOnly = $this->IsReadOnly() ? 'readonly="readonly"' : '';
$aResult = array('label' => $this->sLabel, 'value' => "<input type=\"hidden\" id=\"$sId\" name=\"$sName\" $sReadOnly value=\"".htmlentities($this->defaultValue, ENT_QUOTES, 'UTF-8')."\">");
$sJSFields = json_encode(array_keys($this->aAllowedValues));
$oP->add_ready_script(
<<<EOF
$('#collapse_$sId').click(function() { $(this).toggleClass('ui-icon-circle-triangle-s').toggleClass('ui-icon-circle-triangle-e'); $('#fieldsbasket_$sId').toggle(); $('#trash_icon_$sId').toggle(); $('#trash_$sId').toggle(); } );
$('#fieldsbasket_$sId .movable_attr').draggable({connectToSortable: '#sortable_$sId', helper: 'clone', revert: false });
$('#recycle_$sId').sortable({ receive: function(event, ui) { ui.item.animate({opacity: 0.25}, { complete: function() { $(this).remove(); } });} });
$('#sortable_$sId').sortable({connectWith: '#recycle_$sId', forcePlaceholderSize: true});
$('#sortable_$sId').disableSelection();
EOF
"$('#$sId').sortable_field({aAvailableFields: $sJSFields});"
);
return array('label' => $this->sLabel, 'value' => $sHtml);
return $aResult;
}
}
@@ -944,15 +1192,44 @@ class DesignerFormSelectorField extends DesignerFormField
$sName = $this->oForm->GetFieldName($this->sCode);
$sReadOnly = $this->IsReadOnly() ? 'disabled="disabled"' : '';
$sHtml = "<select id=\"$sId\" name=\"$sName\" $sReadOnly>";
foreach($this->aSubForms as $sKey => $aFormData)
$sCSSClasses = '';
if (count($this->aCSSClasses) > 0)
{
$sDisplayValue = $aFormData['label'];
$sSelected = ($sKey == $this->defaultValue) ? 'selected' : '';
$sHtml .= "<option value=\"".htmlentities($sKey, ENT_QUOTES, 'UTF-8')."\" $sSelected>".htmlentities($sDisplayValue, ENT_QUOTES, 'UTF-8')."</option>";
$sCSSClasses = 'class="'.implode(' ', $this->aCSSClasses).'"';
}
$sHtml .= "</select>";
if ($this->IsReadOnly())
{
$aSelected = array();
$aHiddenValues = array();
$sDisplayValue = '';
$sHiddenValue = '';
foreach($this->aSubForms as $sKey => $aFormData)
{
if ($sKey == $this->defaultValue)
{
$sDisplayValue = htmlentities($aFormData['label'], ENT_QUOTES, 'UTF-8');
$sHiddenValue = "<input type=\"hidden\" id=\"$sId\" name=\"$sName\" value=\"".htmlentities($sKey, ENT_QUOTES, 'UTF-8')."\"/>";
break;
}
}
$sHtml = "<span $sCSSClasses>".$sDisplayValue.$sHiddenValue."</span>";
}
else
{
$sHtml = "<select $sCSSClasses id=\"$sId\" name=\"$sName\" $sReadOnly>";
foreach($this->aSubForms as $sKey => $aFormData)
{
$sDisplayValue = htmlentities($aFormData['label'], ENT_QUOTES, 'UTF-8');;
$sSelected = ($sKey == $this->defaultValue) ? 'selected' : '';
$sHtml .= "<option value=\"".htmlentities($sKey, ENT_QUOTES, 'UTF-8')."\" $sSelected>".$sDisplayValue."</option>";
}
$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>';
@@ -961,7 +1238,7 @@ class DesignerFormSelectorField extends DesignerFormField
foreach($this->aSubForms as $sKey => $aFormData)
{
$sId = $this->oForm->GetFieldId($this->sCode);
$sStyle = ($sKey == $this->defaultValue) ? '' : 'style="display:none"';
$sStyle = (($sKey == $this->defaultValue) && $this->oForm->IsDisplayed()) ? '' : 'style="display:none"';
$oSubForm = $aFormData['form'];
$oSubForm->SetParentForm($this->oForm);
$oSubForm->CopySubmitParams($this->oForm);
@@ -969,22 +1246,77 @@ class DesignerFormSelectorField extends DesignerFormField
if ($sRenderMode == 'property')
{
$sHtml .= "</tbody><tbody class=\"subform\" id=\"{$sId}_{$sKey}\" $sStyle>";
$sHtml .= $oSubForm->RenderAsPropertySheet($oP, true);
// 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).
// The div hierarchy has been emulated by adding attributes to the tbody tags:
// - data-selector : uniquely identifies the DesignerFormSelectorField that has an impact on the visibility of the node
// - data-path : uniquely identifies the combination of users choices that must be made to show the node
// - data-state : records the state, depending on the user choice on the FormSelectorField just above the node, but indepentantly from the visibility in the page (can be visible in the form itself being in a hidden form)
// Then a series of actions are performed to hide and show the relevant nodes, depending on the user choice
$sSelector = $this->oForm->GetHierarchyPath().'/'.$this->sCode;
$oSubForm->SetHierarchyParent($sSelector);
$sPath = $this->oForm->GetHierarchyPath().'/'.$this->sCode.':'.$sKey;
$oSubForm->SetHierarchyPath($sPath);
$oSubForm->SetDisplayed($sKey == $this->defaultValue);
$sState = ($sKey == $this->defaultValue) ? 'visible' : 'hidden';
$sHtml .= "</tbody><tbody data-selector=\"$sSelector\" data-path=\"$sPath\" data-state=\"$sState\" $sStyle>";
$sHtml .= $oSubForm->RenderAsPropertySheet($oP, true);
$sState = $this->oForm->IsDisplayed() ? 'visible' : 'hidden';
if ($oParent = $this->oForm->GetParentForm())
{
$sParentSelector = $oParent->GetHierarchyParent();
$sParentPath = $oParent->GetHierarchyPath();
}
else
{
$sParentSelector = '';
$sParentPath = '';
}
$sHtml .= "</tbody><tbody data-selector=\"$sParentSelector\" data-path=\"$sParentPath\" data-state=\"$sState\" $sStyle>";
}
else
{
$sHtml .= "<div class=\"subform\" id=\"{$sId}_{$sKey}\" $sStyle>";
$sHtml .= "<div class=\"subform_{$sId} {$sId}_{$sKey}\" $sStyle>";
$sHtml .= $oSubForm->Render($oP, true);
$sHtml .= "</div>";
}
}
$oP->add_ready_script(
if ($sRenderMode == 'property')
{
$sSelector = $this->oForm->GetHierarchyPath().'/'.$this->sCode;
$oP->add_ready_script(
<<<EOF
$('#$sId').bind('change reverted', function() { $('.subform').hide(); $('#{$sId}_'+this.value).show(); } );
$('#$sId').bind('change reverted', function() {
// Mark all the direct children as hidden
$('tbody[data-selector="$sSelector"]').attr('data-state', 'hidden');
// Mark the selected one as visible
var sSelectedHierarchy = '$sSelector:'+this.value;
$('tbody[data-path="'+sSelectedHierarchy+'"]').attr('data-state', 'visible');
// Show all items behind the current one
$('tbody[data-path^="$sSelector"]').show();
// Hide items behind the current one as soon as it is behind a hidden node (or itself is marked as hidden)
$('tbody[data-path^="$sSelector"][data-state="hidden"]').each(function(){
$(this).hide();
var sPath = $(this).attr('data-path');
$('tbody[data-path^="'+sPath+'/"]').hide();
});
});
EOF
);
);
}
else
{
$oP->add_ready_script(
<<<EOF
$('#$sId').bind('change reverted', function() { $('.subform_{$sId}').hide(); $('.{$sId}_'+this.value).show(); } );
EOF
);
}
return array('label' => $this->sLabel, 'value' => $sHtml);
}

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2014 Combodo SARL
//
// This file is part of iTop.
//
@@ -20,7 +20,7 @@
/**
* Class iTopWebPage
*
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @copyright Copyright (C) 2010-2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -30,25 +30,21 @@ require_once(APPROOT."/application/user.preferences.class.inc.php");
/**
* Web page with some associated CSS and scripts (jquery) for a fancier display
*/
class iTopWebPage extends NiceWebPage
class iTopWebPage extends NiceWebPage implements iTabbedPage
{
private $m_sMenu;
// private $m_currentOrganization;
private $m_aTabs;
private $m_sCurrentTabContainer;
private $m_sCurrentTab;
private $m_sMessage;
private $m_sInitScript;
protected $m_oTabs;
public function __construct($sTitle)
{
parent::__construct($sTitle);
$this->m_oTabs = new TabManager();
ApplicationContext::SetUrlMakerClass('iTopStandardURLMaker');
$this->m_sCurrentTabContainer = '';
$this->m_sCurrentTab = '';
$this->m_aTabs = array();
$this->m_sMenu = "";
$this->m_sMessage = '';
$this->SetRootUrl(utils::GetAbsoluteUrlAppRoot());
@@ -80,8 +76,18 @@ class iTopWebPage extends NiceWebPage
$this->add_linked_script('../js/jquery.multiselect.min.js');
$this->add_linked_script('../js/ajaxfileupload.js');
$aMultiselectOptions = array(
'header' => true,
'checkAllText' => Dict::S('UI:SearchValue:CheckAll'),
'uncheckAllText' => Dict::S('UI:SearchValue:UncheckAll'),
'noneSelectedText' => Dict::S('UI:SearchValue:Any'),
'selectedText' => Dict::S('UI:SearchValue:NbSelected'),
'selectedList' => 1,
);
$sJSMultiselectOptions = json_encode($aMultiselectOptions);
$sSearchAny = addslashes(Dict::S('UI:SearchValue:Any'));
$sSearchNbSelected = addslashes(Dict::S('UI:SearchValue:NbSelected'));
$this->add_dict_entry('UI:FillAllMandatoryFields');
$bForceMenuPane = utils::ReadParam('force_menu_pane', null);
$sInitClosed = '';
@@ -89,6 +95,35 @@ class iTopWebPage extends NiceWebPage
{
$sInitClosed = 'initClosed: true,';
}
$this->add_script(
<<<EOF
function ShowAboutBox()
{
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'about_box'}, function(data){
$('body').append(data);
});
return false;
}
EOF
);
if (MetaModel::GetConfig()->Get('demo_mode'))
{
// Leave the pane opened
$sConfigureWestPane = '';
}
else
{
$sConfigureWestPane =
<<<EOF
if (GetUserPreference('menu_pane', 'open') == 'closed')
{
myLayout.close('west');
}
myLayout.addPinBtn( "#tPinMenu", "west" );
EOF;
}
$this->m_sInitScript =
<<< EOF
@@ -140,18 +175,15 @@ class iTopWebPage extends NiceWebPage
}
});
myLayout.addPinBtn( "#tPinMenu", "west" );
window.clearTimeout(iPaneVisWatchDog);
//myLayout.open( "west" );
$('.ui-layout-resizer-west .ui-layout-toggler').css({background: 'transparent'});
if (GetUserPreference('menu_pane', 'open') == 'closed')
{
myLayout.close('west');
}
$sConfigureWestPane
$('#left-pane').layout({ resizable: false, spacing_open: 0, south: { size: 94 }, enableCursorHotkey: false });
// Accordion Menu
$("#accordion").accordion({ header: "h3", navigation: true, autoHeight: false, collapsible: false, icons: false }); // collapsible will be enabled once the item will be selected
$("#accordion").accordion({ header: "h3", navigation: true, heightStyle: "content", collapsible: false, icons: false }); // collapsible will be enabled once the item will be selected
// Tabs, using JQuery BBQ to store the history
// The "tab widgets" to handle.
@@ -160,16 +192,42 @@ class iTopWebPage extends NiceWebPage
// This selector will be reused when selecting actual tab widget A elements.
var tab_a_selector = 'ul.ui-tabs-nav a';
// Ugly patch for a change in the behavior of jQuery UI:
// Before jQuery UI 1.9, tabs were always considered as "local" (opposed to Ajax)
// when their href was beginning by #. Starting with 1.9, a <base> tag in the page
// is taken into account and causes "local" tabs to be considered as Ajax
// unless their URL is equal to the URL of the page...
$('div[id^=tabbedContent] > ul > li > a').each(function() {
var sHash = location.hash;
var sHref = $(this).attr("href");
if (sHref.match(/^#/))
{
var sCleanLocation = location.href.toString().replace(sHash, '').replace(/#$/, '');
$(this).attr("href", sCleanLocation+$(this).attr("href"));
}
});
// Enable tabs on all tab widgets. The `event` property must be overridden so
// that the tabs aren't changed on click, and any custom event name can be
// specified. Note that if you define a callback for the 'select' event, it
// will be executed for the selected tab whenever the hash changes.
tabs.tabs({ event: 'change', 'show': function(event, ui) {
tabs.tabs({
event: 'change', 'show': function(event, ui) {
$('.resizable', ui.panel).resizable(); // Make resizable everything that claims to be resizable !
},
beforeLoad: function( event, ui ) {
if ( ui.tab.data('loaded') && (ui.tab.attr('data-cache') == 'true')) {
event.preventDefault();
return;
}
ui.panel.html('<div><img src="../images/indicator.gif"></div>');
ui.jqXHR.success(function() {
ui.tab.data( "loaded", true );
});
}
});
$('.multiselect').multiselect({header: false, noneSelectedText: '$sSearchAny', selectedList: 1, selectedText:'$sSearchNbSelected'});
$('.multiselect').multiselect($sJSMultiselectOptions);
$('.resizable').filter(':visible').resizable();
}
@@ -180,6 +238,7 @@ class iTopWebPage extends NiceWebPage
}
EOF
;
$this->add_ready_script(
<<< EOF
@@ -370,7 +429,7 @@ EOF
{
if ($('#rawOutput > div').html() != '')
{
$('#rawOutput').dialog( {autoOpen: true, modal:false});
$('#rawOutput').dialog( {autoOpen: true, modal:false, width: '80%'});
}
}
@@ -390,6 +449,11 @@ EOF
});
function FixPaneVis()
{
$('.ui-layout-center, .ui-layout-north, .ui-layout-south').css({display: 'block'});
}
EOF
);
}
@@ -480,6 +544,31 @@ EOF
$sForm = $this->GetSiloSelectionForm();
$this->DisplayMenu(); // Compute the menu
// Call the extensions to add content to the page, so that they can also add styles or scripts
$sBannerExtraHtml = '';
foreach (MetaModel::EnumPlugins('iPageUIExtension') as $oExtensionInstance)
{
$sBannerExtraHtml .= $oExtensionInstance->GetBannerHtml($this);
}
$sNorthPane = '';
foreach (MetaModel::EnumPlugins('iPageUIExtension') as $oExtensionInstance)
{
$sNorthPane .= $oExtensionInstance->GetNorthPaneHtml($this);
}
if (UserRights::IsAdministrator() && ExecutionKPI::IsEnabled())
{
$sNorthPane .= '<div id="admin-banner"><span style="padding:5px;">'.ExecutionKPI::GetDescription().'<span></div>';
}
//$sSouthPane = '<p>Peak memory Usage: '.sprintf('%.3f MB', memory_get_peak_usage(true) / (1024*1024)).'</p>';
$sSouthPane = '';
foreach (MetaModel::EnumPlugins('iPageUIExtension') as $oExtensionInstance)
{
$sSouthPane .= $oExtensionInstance->GetSouthPaneHtml($this);
}
// Put here the 'ready scripts' that must be executed after all others
$this->add_ready_script(
<<<EOF
@@ -527,6 +616,7 @@ EOF
if ($this->GetOutputFormat() == 'html')
{
$sHtml .= $this->output_dict_entries(true); // before any script so that they can benefit from the translations
foreach($this->a_linked_scripts as $s_script)
{
// Make sure that the URL to the script contains the application's version number
@@ -541,7 +631,7 @@ EOF
}
$sHtml .= "<script type=\"text/javascript\" src=\"$s_script\"></script>\n";
}
$this->add_script("\$(document).ready(function() {\n{$this->m_sInitScript};\nwindow.setTimeout('onDelayedReady()',10)\n});");
$this->add_script("var iPaneVisWatchDog = window.setTimeout('FixPaneVis()',5000);\n\$(document).ready(function() {\n{$this->m_sInitScript};\nwindow.setTimeout('onDelayedReady()',10)\n});");
if (count($this->m_aReadyScripts)>0)
{
$this->add_script("\nonDelayedReady = function() {\n".implode("\n", $this->m_aReadyScripts)."\n}\n");
@@ -555,7 +645,6 @@ EOF
}
$sHtml .= "</script>\n";
}
$this->output_dict_entries();
}
if (count($this->a_styles)>0)
@@ -573,12 +662,6 @@ EOF
$sHtml .= "</head>\n";
$sHtml .= "<body>\n";
// Render the revision number
if (ITOP_REVISION == '$WCREV$')
{
@@ -603,34 +686,7 @@ EOF
$sOnClick = " onclick=\"this.value='';this.onclick=null;\"";
}
// Render the tabs in the page (if any)
foreach($this->m_aTabs as $sTabContainerName => $m_aTabs)
{
$sTabs = '';
$container_index = 0;
if (count($m_aTabs) > 0)
{
$sTabs = "<!-- tabs -->\n<div id=\"tabbedContent_{$container_index}\" class=\"light\">\n";
$sTabs .= "<ul>\n";
// Display the unordered list that will be rendered as the tabs
$i = 0;
foreach($m_aTabs as $sTabName => $sTabContent)
{
$sTabs .= "<li><a href=\"#tab_$i\" class=\"tab\"><span>".htmlentities($sTabName, ENT_QUOTES, 'UTF-8')."</span></a></li>\n";
$i++;
}
$sTabs .= "</ul>\n";
// Now add the content of the tabs themselves
$i = 0;
foreach($m_aTabs as $sTabName => $sTabContent)
{
$sTabs .= "<div id=\"tab_$i\">".$sTabContent."</div>\n";
$i++;
}
$sTabs .= "</div>\n<!-- end of tabs-->\n";
}
$this->s_content = str_replace("\$Tabs:$sTabContainerName\$", $sTabs, $this->s_content);
$container_index++;
}
$this->s_content = $this->m_oTabs->RenderIntoContent($this->s_content);
if ($this->GetOutputFormat() == 'html')
{
@@ -664,6 +720,10 @@ EOF
$aActions[$oChangePwd->GetUID()] = $oChangePwd->GetMenuItem();
}
utils::GetPopupMenuItems($this, iPopupMenuExtension::MENU_USER_ACTIONS, null, $aActions);
$oAbout = new JSPopupMenuItem('UI:AboutBox', Dict::S('UI:AboutBox'), 'return ShowAboutBox();');
$aActions[$oAbout->GetUID()] = $oAbout->GetMenuItem();
$sLogOffMenu .= $this->RenderPopupMenuItems($aActions);
@@ -699,38 +759,13 @@ EOF
$sApplicationBanner .= '<div id="admin-banner"><span style="padding:5px;">'.$this->m_sMessage.'<span></div>';
}
$sEnvironment = utils::GetCurrentEnvironment();
$sBackButton = utils::GetEnvironmentBackButton();
if($sEnvironment != 'production')
{
$sEnvLabel = trim(MetaModel::GetConfig()->Get('app_env_label'));
if (strlen($sEnvLabel) == 0)
{
$sEnvLabel = $sEnvironment;
}
$sApplicationBanner .= '<div id="admin-banner"><span style="padding:5px;">'.Dict::Format('UI:ApplicationEnvironment', $sEnvLabel).$sBackButton.'<span></div>';
}
$sApplicationBanner .= $sBannerExtraHtml;
foreach (MetaModel::EnumPlugins('iPageUIExtension') as $oExtensionInstance)
{
$sApplicationBanner .= $oExtensionInstance->GetBannerHtml($this);
}
$sNorthPane = '';
foreach (MetaModel::EnumPlugins('iPageUIExtension') as $oExtensionInstance)
{
$sNorthPane .= $oExtensionInstance->GetNorthPaneHtml($this);
}
if (!empty($sNorthPane))
{
$sNorthPane = '<div id="bottom-pane" class="ui-layout-south">'.$sNorthPane.'</div>';
$sNorthPane = '<div id="bottom-pane" class="ui-layout-north">'.$sNorthPane.'</div>';
}
$sSouthPane = '';
foreach (MetaModel::EnumPlugins('iPageUIExtension') as $oExtensionInstance)
{
$sSouthPane .= $oExtensionInstance->GetSouthPaneHtml($this);
}
if (!empty($sSouthPane))
{
$sSouthPane = '<div id="bottom-pane" class="ui-layout-south">'.$sSouthPane.'</div>';
@@ -740,15 +775,24 @@ EOF
$sOnlineHelpUrl = MetaModel::GetConfig()->Get('online_help');
//$sLogOffMenu = "<span id=\"logOffBtn\" style=\"height:55px;padding:0;margin:0;\"><img src=\"../images/onOffBtn.png\"></span>";
$sDisplayIcon = utils::GetAbsoluteUrlAppRoot().'images/itop-logo.png';
if (file_exists(MODULESROOT.'branding/main-logo.png'))
{
$sDisplayIcon = utils::GetAbsoluteUrlModulesRoot().'branding/main-logo.png';
}
$sHtml .= $sNorthPane;
$sHtml .= '<div id="left-pane" class="ui-layout-west">';
$sHtml .= '<!-- Beginning of the left pane -->';
$sHtml .= ' <div class="ui-layout-north">';
$sHtml .= ' <div id="header-logo">';
$sHtml .= ' <div id="top-left"></div><div id="logo"><a href="'.htmlentities($sIconUrl, ENT_QUOTES, 'UTF-8').'"><img src="../images/itop-logo.png" title="'.htmlentities($sVersionString, ENT_QUOTES, 'UTF-8').'" style="border:0; margin-top:16px; margin-right:40px;"/></a></div>';
$sHtml .= ' <div id="top-left"></div><div id="logo"><a href="'.htmlentities($sIconUrl, ENT_QUOTES, 'UTF-8').'"><img src="'.$sDisplayIcon.'" title="'.htmlentities($sVersionString, ENT_QUOTES, 'UTF-8').'" style="border:0; margin-top:16px; margin-right:40px;"/></a></div>';
$sHtml .= ' </div>';
$sHtml .= ' <div class="header-menu">';
$sHtml .= ' <div class="icon ui-state-default ui-corner-all"><span id="tPinMenu" class="ui-icon ui-icon-pin-w">pin</span></div>';
if (!MetaModel::GetConfig()->Get('demo_mode'))
{
$sHtml .= ' <div class="icon ui-state-default ui-corner-all"><span id="tPinMenu" class="ui-icon ui-icon-pin-w">pin</span></div>';
}
$sHtml .= ' <div style="text-align:center;">'.self::FilterXSS($sForm).'</div>';
$sHtml .= ' </div>';
$sHtml .= ' </div>';
@@ -761,7 +805,7 @@ EOF
$sHtml .= ' </div>';
$sHtml .= ' </div> <!-- /inner menu -->';
$sHtml .= ' </div> <!-- /menu -->';
$sHtml .= ' <div class="footer ui-layout-south"><a href="http://www.combodo.com" title="www.combodo.com" target="_blank"><img src="../images/logo-combodo.png"/></a></div>';
$sHtml .= ' <div class="footer ui-layout-south"><div id="combodo_logo"><a href="http://www.combodo.com" title="www.combodo.com" target="_blank"><img src="../images/logo-combodo.png"/></a></div></div>';
$sHtml .= '<!-- End of the left pane -->';
$sHtml .= '</div>';
@@ -774,7 +818,7 @@ EOF
$sHtml .= '<td style="padding-right:20px;padding-left:10px;">'.self::FilterXSS($sLogOffMenu).'</td><td><input type="hidden" name="operation" value="full_text"/></td></tr></table></form></div>';
//echo '<td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<input type="hidden" name="operation" value="full_text"/></td></tr></table></form></div>';
$sHtml .= ' </div>';
$sHtml .= ' <div class="ui-layout-content">';
$sHtml .= ' <div class="ui-layout-content" style="overflow:auto;">';
$sHtml .= ' <!-- Beginning of page content -->';
$sHtml .= self::FilterXSS($this->s_content);
$sHtml .= ' <!-- End of page content -->';
@@ -802,7 +846,9 @@ EOF
if ($this->GetOutputFormat() == 'html')
{
$oKPI = new ExecutionKPI();
echo $sHtml;
$oKPI->ComputeAndReport('Echoing ('.round(strlen($sHtml) / 1024).' Kb)');
}
else if ($this->GetOutputFormat() == 'pdf' && $this->IsOutputFormatAvailable('pdf') )
{
@@ -828,64 +874,54 @@ EOF
$oMPDF->Output($sOutputName, 'I');
}
MetaModel::RecordQueryTrace();
ExecutionKPI::ReportStats();
}
public function AddTabContainer($sTabContainer)
public function AddTabContainer($sTabContainer, $sPrefix = '')
{
$this->m_aTabs[$sTabContainer] = array();
$this->add("\$Tabs:$sTabContainer\$");
$this->add($this->m_oTabs->AddTabContainer($sTabContainer, $sPrefix));
}
public function AddToTab($sTabContainer, $sTabLabel, $sHtml)
{
if (!isset($this->m_aTabs[$sTabContainer][$sTabLabel]))
{
// Set the content of the tab
$this->m_aTabs[$sTabContainer][$sTabLabel] = $sHtml;
}
else
{
// Append to the content of the tab
$this->m_aTabs[$sTabContainer][$sTabLabel] .= $sHtml;
}
$this->add($this->m_oTabs->AddToTab($sTabContainer, $sTabLabel, $sHtml));
}
public function SetCurrentTabContainer($sTabContainer = '')
{
$sPreviousTabContainer = $this->m_sCurrentTabContainer;
$this->m_sCurrentTabContainer = $sTabContainer;
return $sPreviousTabContainer;
return $this->m_oTabs->SetCurrentTabContainer($sTabContainer);
}
public function SetCurrentTab($sTabLabel = '')
{
$sPreviousTab = $this->m_sCurrentTab;
$this->m_sCurrentTab = $sTabLabel;
return $sPreviousTab;
return $this->m_oTabs->SetCurrentTab($sTabLabel);
}
/**
* Add a tab which content will be loaded asynchronously via the supplied URL
*
* Limitations:
* Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to pull content from another server.
* Static content cannot be added inside such tabs.
*
* @param string $sTabLabel The (localised) label of the tab
* @param string $sUrl The URL to load (on the same server)
* @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause the tab to be reloaded upon each activation.
* @since 2.0.3
*/
public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true)
{
$this->add($this->m_oTabs->AddAjaxTab($sTabLabel, $sUrl, $bCache));
}
public function GetCurrentTab()
{
return $this->m_sCurrentTab;
return $this->m_oTabs->GetCurrentTab();
}
public function RemoveTab($sTabLabel, $sTabContainer = null)
{
if ($sTabContainer == null)
{
$sTabContainer = $this->m_sCurrentTabContainer;
}
if (isset($this->m_aTabs[$sTabContainer][$sTabLabel]))
{
// Delete the content of the tab
unset($this->m_aTabs[$sTabContainer][$sTabLabel]);
// If we just removed the active tab, let's reset the active tab
if (($this->m_sCurrentTabContainer == $sTabContainer) && ($this->m_sCurrentTab == $sTabLabel))
{
$this->m_sCurrentTab = '';
}
}
$this->m_oTabs->RemoveTab($sTabLabel, $sTabContainer);
}
/**
@@ -894,20 +930,7 @@ EOF
*/
public function FindTab($sPattern, $sTabContainer = null)
{
$return = false;
if ($sTabContainer == null)
{
$sTabContainer = $this->m_sCurrentTabContainer;
}
foreach($this->m_aTabs[$sTabContainer] as $sTabLabel => $void)
{
if (preg_match($sPattern, $sTabLabel))
{
$result = $sTabLabel;
break;
}
}
return $result;
return $this->m_oTabs->FindTab($sPattern, $sTabContainer);
}
/**
@@ -918,26 +941,7 @@ EOF
*/
public function SelectTab($sTabContainer, $sTabLabel)
{
$container_index = 0;
$tab_index = 0;
foreach($this->m_aTabs as $sCurrentTabContainerName => $aTabs)
{
if ($sTabContainer == $sCurrentTabContainerName)
{
foreach($aTabs as $sCurrentTabLabel => $void)
{
if ($sCurrentTabLabel == $sTabLabel)
{
break;
}
$tab_index++;
}
break;
}
$container_index++;
}
$sSelector = '#tabbedContent_'.$container_index.' > ul';
$this->add_ready_script("$('$sSelector').tabs('select', $tab_index);");
$this->add_ready_script($this->m_oTabs->SelectTab($sTabContainer, $sTabLabel));
}
public function StartCollapsibleSection($sSectionLabel, $bOpen = false)
@@ -971,9 +975,9 @@ EOF
public function add($sHtml)
{
if (!empty($this->m_sCurrentTabContainer) && !empty($this->m_sCurrentTab))
if (($this->m_oTabs->GetCurrentTabContainer() != '') && ($this->m_oTabs->GetCurrentTab() != ''))
{
$this->AddToTab($this->m_sCurrentTabContainer, $this->m_sCurrentTab, $sHtml);
$this->m_oTabs->AddToCurrentTab($sHtml);
}
else
{
@@ -987,10 +991,13 @@ EOF
*/
public function start_capture()
{
if (!empty($this->m_sCurrentTabContainer) && !empty($this->m_sCurrentTab))
$sCurrentTabContainer = $this->m_oTabs->GetCurrentTabContainer();
$sCurrentTab = $this->m_oTabs->GetCurrentTab();
if (!empty($sCurrentTabContainer) && !empty($sCurrentTab))
{
$iOffset = isset($this->m_aTabs[$this->m_sCurrentTabContainer][$this->m_sCurrentTab]) ? strlen($this->m_aTabs[$this->m_sCurrentTabContainer][$this->m_sCurrentTab]): 0;
return array('tc' => $this->m_sCurrentTabContainer, 'tab' => $this->m_sCurrentTab, 'offset' => $iOffset);
$iOffset = $this->m_oTabs->GetCurrentTabLength();
return array('tc' => $sCurrentTabContainer, 'tab' => $sCurrentTab, 'offset' => $iOffset);
}
else
{
@@ -1008,10 +1015,9 @@ EOF
{
if (is_array($offset))
{
if (isset($this->m_aTabs[$offset['tc']][$offset['tab']]))
if ($this->m_oTabs->TabExists($offset['tc'], $offset['tab']))
{
$sCaptured = substr($this->m_aTabs[$offset['tc']][$offset['tab']], $offset['offset']);
$this->m_aTabs[$offset['tc']][$offset['tab']] = substr($this->m_aTabs[$offset['tc']][$offset['tab']], 0, $offset['offset']);
$sCaptured = $this->m_oTabs->TruncateTab($offset['tc'], $offset['tab'], $offset['offset']);
}
else
{

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -20,7 +20,7 @@
/**
* Class LoginWebPage
*
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @copyright Copyright (C) 2010-2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -31,72 +31,74 @@ require_once(APPROOT."/application/nicewebpage.class.inc.php");
class LoginWebPage extends NiceWebPage
{
const EXIT_PROMPT = 0;
const EXIT_HTTP_401 = 1;
const EXIT_RETURN = 2;
const EXIT_CODE_OK = 0;
const EXIT_CODE_MISSINGLOGIN = 1;
const EXIT_CODE_MISSINGPASSWORD = 2;
const EXIT_CODE_WRONGCREDENTIALS = 3;
const EXIT_CODE_MUSTBEADMIN = 4;
const EXIT_CODE_PORTALUSERNOTAUTHORIZED = 5;
protected static $sHandlerClass = __class__;
public static function RegisterHandler($sClass)
{
self::$sHandlerClass = $sClass;
}
public static function NewLoginWebPage()
{
return new self::$sHandlerClass;
}
protected static $m_sLoginFailedMessage = '';
public function __construct()
{
parent::__construct("iTop Login");
$this->add_style(<<<EOF
body {
background: #eee;
margin: 0;
padding: 0;
}
#login-logo {
margin-top: 150px;
width: 300px;
padding-left: 20px;
padding-right: 20px;
padding-top: 10px;
padding-bottom: 10px;
margin-left: auto;
margin-right: auto;
background: #f6f6f1;
height: 54px;
border-top: 1px solid #000;
border-left: 1px solid #000;
border-right: 1px solid #000;
border-bottom: 0;
text-align: center;
}
#login-logo img {
border: 0;
}
#login {
width: 300px;
margin-left: auto;
margin-right: auto;
padding: 20px;
background-color: #fff;
border-bottom: 1px solid #000;
border-left: 1px solid #000;
border-right: 1px solid #000;
border-top: 0;
text-align: center;
}
#pwd, #user,#old_pwd, #new_pwd, #retype_new_pwd {
width: 10em;
}
.center {
text-align: center;
}
h1 {
color: #1C94C4;
font-size: 16pt;
}
.v-spacer {
padding-top: 1em;
}
EOF
);
public function __construct($sTitle = 'iTop Login')
{
parent::__construct($sTitle);
$this->SetStyleSheet();
$this->add_header("Cache-control: no-cache");
}
public function SetStyleSheet()
{
$this->add_linked_stylesheet("../css/login.css");
}
public static function SetLoginFailedMessage($sMessage)
{
self::$m_sLoginFailedMessage = $sMessage;
}
public function EnableResetPassword()
{
return MetaModel::GetConfig()->Get('forgot_password');
}
public function DisplayLoginHeader($bMainAppLogo = false)
{
if ($bMainAppLogo)
{
$sLogo = 'itop-logo.png';
$sBrandingLogo = 'main-logo.png';
}
else
{
$sLogo = 'itop-logo-external.png';
$sBrandingLogo = 'login-logo.png';
}
$sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_VERSION);
$sIconUrl = Utils::GetConfig()->Get('app_icon_url');
$sDisplayIcon = utils::GetAbsoluteUrlAppRoot().'images/'.$sLogo;
if (file_exists(MODULESROOT.'branding/'.$sBrandingLogo))
{
$sDisplayIcon = utils::GetAbsoluteUrlModulesRoot().'branding/'.$sBrandingLogo;
}
$this->add("<div id=\"login-logo\"><a href=\"".htmlentities($sIconUrl, ENT_QUOTES, 'UTF-8')."\"><img title=\"$sVersionShort\" src=\"$sDisplayIcon\"></a></div>\n");
}
public function DisplayLoginForm($sLoginType, $bFailedLogin = false)
{
switch($sLoginType)
@@ -111,6 +113,7 @@ EOF
case 'url':
$this->add_header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_VERSION));
$this->add_header('HTTP/1.0 401 Unauthorized');
$this->add_header('Content-type: text/html; charset=iso-8859-1');
// Note: displayed when the user will click on Cancel
$this->add('<p><strong>'.Dict::S('UI:Login:Error:AccessRestricted').'</strong></p>');
break;
@@ -121,9 +124,7 @@ EOF
$sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data');
$sAuthPwd = utils::ReadParam('suggest_pwd', '', true, 'raw_data');
$sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_VERSION);
$sIconUrl = Utils::GetConfig()->Get('app_icon_url');
$this->add("<div id=\"login-logo\"><a href=\"".htmlentities($sIconUrl, ENT_QUOTES, 'UTF-8')."\"><img title=\"$sVersionShort\" src=\"../images/itop-logo-external.png\"></a></div>\n");
$this->DisplayLoginHeader();
$this->add("<div id=\"login\">\n");
$this->add("<h1>".Dict::S('UI:Login:Welcome')."</h1>\n");
if ($bFailedLogin)
@@ -142,12 +143,39 @@ EOF
$this->add("<p>".Dict::S('UI:Login:IdentifyYourself')."</p>\n");
}
$this->add("<form method=\"post\">\n");
$this->add("<table width=\"100%\">\n");
$this->add("<table>\n");
$sForgotPwd = $this->EnableResetPassword() ? $this->ForgotPwdLink() : '';
$this->add("<tr><td style=\"text-align:right\"><label for=\"user\">".Dict::S('UI:Login:UserNamePrompt').":</label></td><td style=\"text-align:left\"><input id=\"user\" type=\"text\" name=\"auth_user\" value=\"".htmlentities($sAuthUser, ENT_QUOTES, 'UTF-8')."\" /></td></tr>\n");
$this->add("<tr><td style=\"text-align:right\"><label for=\"pwd\">".Dict::S('UI:Login:PasswordPrompt').":</label></td><td style=\"text-align:left\"><input id=\"pwd\" type=\"password\" name=\"auth_pwd\" value=\"".htmlentities($sAuthPwd, ENT_QUOTES, 'UTF-8')."\" /></td></tr>\n");
$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\"> <input type=\"submit\" value=\"".Dict::S('UI:Button:Login')."\" /></td></tr>\n");
$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\"><span class=\"btn_border\"><input type=\"submit\" value=\"".Dict::S('UI:Button:Login')."\" /></span></td></tr>\n");
if (strlen($sForgotPwd) > 0)
{
$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\">$sForgotPwd</td></tr>\n");
}
$this->add("</table>\n");
$this->add("<input type=\"hidden\" name=\"loginop\" value=\"login\" />\n");
$this->add_ready_script('$("#user").focus();');
// Keep the OTHER parameters posted
foreach($_POST as $sPostedKey => $postedValue)
{
if (!in_array($sPostedKey, array('auth_user', 'auth_pwd')))
{
if (is_array($postedValue))
{
foreach($postedValue as $sKey => $sValue)
{
$this->add("<input type=\"hidden\" name=\"".htmlentities($sPostedKey, ENT_QUOTES, 'UTF-8')."[".htmlentities($sKey, ENT_QUOTES, 'UTF-8')."]\" value=\"".htmlentities($sValue, ENT_QUOTES, 'UTF-8')."\" />\n");
}
}
else
{
$this->add("<input type=\"hidden\" name=\"".htmlentities($sPostedKey, ENT_QUOTES, 'UTF-8')."\" value=\"".htmlentities($postedValue, ENT_QUOTES, 'UTF-8')."\" />\n");
}
}
}
$this->add("</form>\n");
$this->add(Dict::S('UI:Login:About'));
$this->add("</div>\n");
@@ -155,11 +183,201 @@ EOF
}
}
/**
* Return '' to disable this feature
*/
public function ForgotPwdLink()
{
$sUrl = '?loginop=forgot_pwd';
$sHtml = "<a href=\"$sUrl\" target=\"_blank\">".Dict::S('UI:Login:ForgotPwd')."</a>";
return $sHtml;
}
public function DisplayForgotPwdForm($bFailedToReset = false, $sFailureReason = null)
{
$this->DisplayLoginHeader();
$this->add("<div id=\"login\">\n");
$this->add("<h1>".Dict::S('UI:Login:ForgotPwdForm')."</h1>\n");
$this->add("<p>".Dict::S('UI:Login:ForgotPwdForm+')."</p>\n");
if ($bFailedToReset)
{
$this->add("<p class=\"hilite\">".Dict::Format('UI:Login:ResetPwdFailed', htmlentities($sFailureReason, ENT_QUOTES, 'UTF-8'))."</p>\n");
}
$sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data');
$this->add("<form method=\"post\">\n");
$this->add("<table>\n");
$this->add("<tr><td colspan=\"2\" class=\"center\"><label for=\"user\">".Dict::S('UI:Login:UserNamePrompt').":</label><input id=\"user\" type=\"text\" name=\"auth_user\" value=\"".htmlentities($sAuthUser, ENT_QUOTES, 'UTF-8')."\" /></td></tr>\n");
$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\"><span class=\"btn_border\"><input type=\"button\" onClick=\"window.close();\" value=\"".Dict::S('UI:Button:Cancel')."\" /></span>&nbsp;&nbsp;<span class=\"btn_border\"><input type=\"submit\" value=\"".Dict::S('UI:Login:ResetPassword')."\" /></span></td></tr>\n");
$this->add("</table>\n");
$this->add("<input type=\"hidden\" name=\"loginop\" value=\"forgot_pwd_go\" />\n");
$this->add("</form>\n");
$this->add("</div>\n");
$this->add_ready_script('$("#user").focus();');
}
protected function ForgotPwdGo()
{
$sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data');
try
{
UserRights::Login($sAuthUser); // Set the user's language (if possible!)
$oUser = UserRights::GetUserObject();
if ($oUser == null)
{
throw new Exception(Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser));
}
if (!MetaModel::IsValidAttCode(get_class($oUser), 'reset_pwd_token'))
{
throw new Exception(Dict::S('UI:ResetPwd-Error-NotPossible'));
}
if (!$oUser->CanChangePassword())
{
throw new Exception(Dict::S('UI:ResetPwd-Error-FixedPwd'));
}
$sTo = $oUser->GetResetPasswordEmail(); // throws Exceptions if not allowed
if ($sTo == '')
{
throw new Exception(Dict::S('UI:ResetPwd-Error-NoEmail'));
}
// This token allows the user to change the password without knowing the previous one
$sToken = substr(md5(APPROOT.uniqid()), 0, 16);
$oUser->Set('reset_pwd_token', $sToken);
CMDBObject::SetTrackInfo('Reset password');
$oUser->DBUpdate();
$oEmail = new Email();
$oEmail->SetRecipientTO($sTo);
$sFrom = MetaModel::GetConfig()->Get('forgot_password_from');
if ($sFrom == '')
{
$sFrom = $sTo;
}
$oEmail->SetRecipientFrom($sFrom);
$oEmail->SetSubject(Dict::S('UI:ResetPwd-EmailSubject'));
$sResetUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?loginop=reset_pwd&auth_user='.urlencode($oUser->Get('login')).'&token='.urlencode($sToken);
$oEmail->SetBody(Dict::Format('UI:ResetPwd-EmailBody', $sResetUrl));
$iRes = $oEmail->Send($aIssues, true /* force synchronous exec */);
switch ($iRes)
{
//case EMAIL_SEND_PENDING:
case EMAIL_SEND_OK:
break;
case EMAIL_SEND_ERROR:
default:
IssueLog::Error('Failed to send the email with the NEW password for '.$oUser->Get('friendlyname').': '.implode(', ', $aIssues));
throw new Exception(Dict::S('UI:ResetPwd-Error-Send'));
}
$this->DisplayLoginHeader();
$this->add("<div id=\"login\">\n");
$this->add("<h1>".Dict::S('UI:Login:ForgotPwdForm')."</h1>\n");
$this->add("<p>".Dict::S('UI:ResetPwd-EmailSent')."</p>");
$this->add("<form method=\"post\">\n");
$this->add("<table>\n");
$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\"><input type=\"button\" onClick=\"window.close();\" value=\"".Dict::S('UI:Button:Done')."\" /></td></tr>\n");
$this->add("</table>\n");
$this->add("</form>\n");
$this->add("</div\n");
}
catch(Exception $e)
{
$this->DisplayForgotPwdForm(true, $e->getMessage());
}
}
public function DisplayResetPwdForm()
{
$sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data');
$sToken = utils::ReadParam('token', '', false, 'raw_data');
UserRights::Login($sAuthUser); // Set the user's language
$oUser = UserRights::GetUserObject();
$this->DisplayLoginHeader();
$this->add("<div id=\"login\">\n");
$this->add("<h1>".Dict::S('UI:ResetPwd-Title')."</h1>\n");
if ($oUser == null)
{
$this->add("<p>".Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)."</p>\n");
}
elseif ($oUser->Get('reset_pwd_token') != $sToken)
{
$this->add("<p>".Dict::S('UI:ResetPwd-Error-InvalidToken')."</p>\n");
}
else
{
$this->add("<p>".Dict::Format('UI:ResetPwd-Error-EnterPassword', $oUser->GetFriendlyName())."</p>\n");
$sInconsistenPwdMsg = Dict::S('UI:Login:RetypePwdDoesNotMatch');
$this->add_script(
<<<EOF
function DoCheckPwd()
{
if ($('#new_pwd').val() != $('#retype_new_pwd').val())
{
alert('$sInconsistenPwdMsg');
return false;
}
return true;
}
EOF
);
$this->add("<form method=\"post\">\n");
$this->add("<table>\n");
$this->add("<tr><td style=\"text-align:right\"><label for=\"new_pwd\">".Dict::S('UI:Login:NewPasswordPrompt').":</label></td><td style=\"text-align:left\"><input type=\"password\" id=\"new_pwd\" name=\"new_pwd\" value=\"\" /></td></tr>\n");
$this->add("<tr><td style=\"text-align:right\"><label for=\"retype_new_pwd\">".Dict::S('UI:Login:RetypeNewPasswordPrompt').":</label></td><td style=\"text-align:left\"><input type=\"password\" id=\"retype_new_pwd\" name=\"retype_new_pwd\" value=\"\" /></td></tr>\n");
$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\"><span class=\"btn_border\"><input type=\"submit\" onClick=\"return DoCheckPwd();\" value=\"".Dict::S('UI:Button:ChangePassword')."\" /></span></td></tr>\n");
$this->add("</table>\n");
$this->add("<input type=\"hidden\" name=\"loginop\" value=\"do_reset_pwd\" />\n");
$this->add("<input type=\"hidden\" name=\"auth_user\" value=\"".htmlentities($sAuthUser, ENT_QUOTES, 'UTF-8')."\" />\n");
$this->add("<input type=\"hidden\" name=\"token\" value=\"".htmlentities($sToken, ENT_QUOTES, 'UTF-8')."\" />\n");
$this->add("</form>\n");
$this->add("</div\n");
}
}
public function DoResetPassword()
{
$sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data');
$sToken = utils::ReadParam('token', '', false, 'raw_data');
$sNewPwd = utils::ReadPostedParam('new_pwd', '', false, 'raw_data');
UserRights::Login($sAuthUser); // Set the user's language
$oUser = UserRights::GetUserObject();
$this->DisplayLoginHeader();
$this->add("<div id=\"login\">\n");
$this->add("<h1>".Dict::S('UI:ResetPwd-Title')."</h1>\n");
if ($oUser == null)
{
$this->add("<p>".Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)."</p>\n");
}
elseif ($oUser->Get('reset_pwd_token') != $sToken)
{
$this->add("<p>".Dict::S('UI:ResetPwd-Error-InvalidToken')."</p>\n");
}
else
{
// Trash the token and change the password
$oUser->Set('reset_pwd_token', '');
$oUser->SetPassword($sNewPwd); // Does record the change into the DB
$this->add("<p>".Dict::S('UI:ResetPwd-Ready')."</p>");
$sUrl = utils::GetAbsoluteUrlAppRoot();
$this->add("<p><a href=\"$sUrl\">".Dict::S('UI:ResetPwd-Login')."</a></p>");
}
$this->add("</div\n");
}
public function DisplayChangePwdForm($bFailedLogin = false)
{
$sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data');
$sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_VERSION);
$sInconsistenPwdMsg = Dict::S('UI:Login:RetypePwdDoesNotMatch');
$this->add_script(<<<EOF
function GoBack()
@@ -178,8 +396,7 @@ function DoCheckPwd()
}
EOF
);
$sIconUrl = Utils::GetConfig()->Get('app_icon_url');
$this->add("<div id=\"login-logo\"><a href=\"".htmlentities($sIconUrl, ENT_QUOTES, 'UTF-8')."\"><img title=\"$sVersionShort\" src=\"../images/itop-logo.png\"></a></div>\n");
$this->DisplayLoginHeader();
$this->add("<div id=\"login\">\n");
$this->add("<h1>".Dict::S('UI:Login:ChangeYourPassword')."</h1>\n");
if ($bFailedLogin)
@@ -187,11 +404,11 @@ EOF
$this->add("<p class=\"hilite\">".Dict::S('UI:Login:IncorrectOldPassword')."</p>\n");
}
$this->add("<form method=\"post\">\n");
$this->add("<table width=\"100%\">\n");
$this->add("<table>\n");
$this->add("<tr><td style=\"text-align:right\"><label for=\"old_pwd\">".Dict::S('UI:Login:OldPasswordPrompt').":</label></td><td style=\"text-align:left\"><input type=\"password\" id=\"old_pwd\" name=\"old_pwd\" value=\"\" /></td></tr>\n");
$this->add("<tr><td style=\"text-align:right\"><label for=\"new_pwd\">".Dict::S('UI:Login:NewPasswordPrompt').":</label></td><td style=\"text-align:left\"><input type=\"password\" id=\"new_pwd\" name=\"new_pwd\" value=\"\" /></td></tr>\n");
$this->add("<tr><td style=\"text-align:right\"><label for=\"retype_new_pwd\">".Dict::S('UI:Login:RetypeNewPasswordPrompt').":</label></td><td style=\"text-align:left\"><input type=\"password\" id=\"retype_new_pwd\" name=\"retype_new_pwd\" value=\"\" /></td></tr>\n");
$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\"> <input type=\"button\" onClick=\"GoBack();\" value=\"".Dict::S('UI:Button:Cancel')."\" />&nbsp;&nbsp;<input type=\"submit\" onClick=\"return DoCheckPwd();\" value=\"".Dict::S('UI:Button:ChangePassword')."\" /></td></tr>\n");
$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\"><span class=\"btn_border\"><input type=\"button\" onClick=\"GoBack();\" value=\"".Dict::S('UI:Button:Cancel')."\" /></span>&nbsp;&nbsp;<span class=\"btn_border\"><input type=\"submit\" onClick=\"return DoCheckPwd();\" value=\"".Dict::S('UI:Button:ChangePassword')."\" /></span></td></tr>\n");
$this->add("</table>\n");
$this->add("<input type=\"hidden\" name=\"loginop\" value=\"do_change_pwd\" />\n");
$this->add("</form>\n");
@@ -220,20 +437,33 @@ EOF
return MetaModel::GetConfig()->GetSecureConnectionRequired();
}
static function IsConnectionSecure()
/**
* Guess if a string looks like an UTF-8 string based on some ranges of multi-bytes encoding
* @param string $sString
* @return bool True if the string contains some typical UTF-8 multi-byte sequences
*/
static function LooksLikeUTF8($sString)
{
$bSecured = false;
if (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS']!='off'))
{
$bSecured = true;
}
return $bSecured;
return preg_match('%(?:
[\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
|\xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
|\xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
|\xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
|[\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
|\xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
)+%xs', $sString);
}
protected static function Login()
/**
* Attempt a login
*
* @param int iOnExit What action to take if the user is not logged on (one of the class constants EXIT_...)
* @return int One of the class constants EXIT_CODE_...
*/
protected static function Login($iOnExit)
{
if (self::SecureConnectionRequired() && !self::IsConnectionSecure())
if (self::SecureConnectionRequired() && !utils::IsConnectionSecure())
{
// Non secured URL... request for a secure connection
throw new Exception('Secure connection required!');
@@ -246,7 +476,7 @@ EOF
//echo "User: ".$_SESSION['auth_user']."\n";
// Already authentified
UserRights::Login($_SESSION['auth_user']); // Login & set the user's language
return true;
return self::EXIT_CODE_OK;
}
else
{
@@ -273,8 +503,8 @@ EOF
case 'form':
// iTop standard mode: form based authentication
$sAuthUser = utils::ReadPostedParam('auth_user', '', false, 'raw_data');
$sAuthPwd = utils::ReadPostedParam('auth_pwd', '', false, 'raw_data');
if ($sAuthUser != '')
$sAuthPwd = utils::ReadPostedParam('auth_pwd', null, false, 'raw_data');
if (($sAuthUser != '') && ($sAuthPwd !== null))
{
$sLoginMode = 'form';
}
@@ -291,7 +521,22 @@ EOF
else if (isset($_SERVER['PHP_AUTH_USER']))
{
$sAuthUser = $_SERVER['PHP_AUTH_USER'];
// Unfortunately, the RFC is not clear about the encoding...
// IE and FF supply the user and password encoded in ISO-8859-1 whereas Chrome provides them encoded in UTF-8
// So let's try to guess if it's an UTF-8 string or not... fortunately all encodings share the same ASCII base
if (!self::LooksLikeUTF8($sAuthUser))
{
// Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8
// Supposed to be harmless in case of a plain ASCII string...
$sAuthUser = iconv('iso-8859-1', 'utf-8', $sAuthUser);
}
$sAuthPwd = $_SERVER['PHP_AUTH_PW'];
if (!self::LooksLikeUTF8($sAuthPwd))
{
// Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8
// Supposed to be harmless in case of a plain ASCII string...
$sAuthPwd = iconv('iso-8859-1', 'utf-8', $sAuthPwd);
}
$sLoginMode = 'basic';
}
break;
@@ -313,7 +558,7 @@ EOF
// Credentials passed directly in the url
$sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data');
$sAuthPwd = utils::ReadParam('auth_pwd', null, false, 'raw_data');
if (($sAuthUser != '') && ($sAuthPwd != null))
if (($sAuthUser != '') && ($sAuthPwd !== null))
{
$sLoginMode = 'url';
}
@@ -334,10 +579,31 @@ EOF
{
$sLoginMode = $aAllowedLoginTypes[0]; // First in the list...
}
$oPage = new LoginWebPage();
$oPage->DisplayLoginForm( $sLoginMode, false /* no previous failed attempt */);
$oPage->output();
exit;
if (($iOnExit == self::EXIT_HTTP_401) || ($sLoginMode == 'basic'))
{
header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_VERSION));
header('HTTP/1.0 401 Unauthorized');
header('Content-type: text/html; charset=iso-8859-1');
exit;
}
else if($iOnExit == self::EXIT_RETURN)
{
if (($sAuthUser !== '') && ($sAuthPwd === null))
{
return self::EXIT_CODE_MISSINGPASSWORD;
}
else
{
return self::EXIT_CODE_MISSINGLOGIN;
}
}
else
{
$oPage = self::NewLoginWebPage();
$oPage->DisplayLoginForm( $sLoginMode, false /* no previous failed attempt */);
$oPage->output();
exit;
}
}
else
{
@@ -345,10 +611,24 @@ EOF
{
//echo "Check Credentials returned false for user $sAuthUser!";
self::ResetSession();
$oPage = new LoginWebPage();
$oPage->DisplayLoginForm( $sLoginMode, true /* failed attempt */);
$oPage->output();
exit;
if (($iOnExit == self::EXIT_HTTP_401) || ($sLoginMode == 'basic'))
{
header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_VERSION));
header('HTTP/1.0 401 Unauthorized');
header('Content-type: text/html; charset=iso-8859-1');
exit;
}
else if($iOnExit == self::EXIT_RETURN)
{
return self::EXIT_CODE_WRONGCREDENTIALS;
}
else
{
$oPage = self::NewLoginWebPage();
$oPage->DisplayLoginForm( $sLoginMode, true /* failed attempt */);
$oPage->output();
exit;
}
}
else
{
@@ -369,16 +649,44 @@ EOF
}
}
}
return self::EXIT_CODE_OK;
}
/**
* Overridable: depending on the user, head toward a dedicated portal
* @param bool $bIsAllowedToPortalUsers Whether or not the current page is considered as part of the portal
* @param int $iOnExit How to complete the call: redirect or return a code
*/
protected static function ChangeLocation($bIsAllowedToPortalUsers, $iOnExit = self::EXIT_PROMPT)
{
if ( (!$bIsAllowedToPortalUsers) && (UserRights::IsPortalUser()))
{
if ($iOnExit == self::EXIT_RETURN)
{
return self::EXIT_CODE_PORTALUSERNOTAUTHORIZED;
}
else
{
// No rights to be here, redirect to the portal
header('Location: '.utils::GetAbsoluteUrlAppRoot().'portal/index.php');
}
}
else
{
return self::EXIT_CODE_OK;
}
}
/**
* Check if the user is already authentified, if yes, then performs some additional validations:
* - if $bMustBeAdmin is true, then the user must be an administrator, otherwise an error is displayed
* - if $bIsAllowedToPortalUsers is false and the user has only access to the portal, then the user is redirected to the portal
* @param bool $bMustBeAdmin Whether or not the user must be an admin to access the current page
* @param bool $bIsAllowedToPortalUsers Whether or not the current page is considered as part of the portal
* @param int iOnExit What action to take if the user is not logged on (one of the class constants EXIT_...)
*/
static function DoLogin($bMustBeAdmin = false, $bIsAllowedToPortalUsers = false)
static function DoLogin($bMustBeAdmin = false, $bIsAllowedToPortalUsers = false, $iOnExit = self::EXIT_PROMPT)
{
$sMessage = ''; // In case we need to return a message to the calling web page
$operation = utils::ReadParam('loginop', '');
@@ -402,16 +710,44 @@ EOF
}
}
self::ResetSession();
$oPage = new LoginWebPage();
$oPage = self::NewLoginWebPage();
$oPage->DisplayLoginForm( $sLoginMode, false /* not a failed attempt */);
$oPage->output();
exit;
}
else if ($operation == 'forgot_pwd')
{
$oPage = self::NewLoginWebPage();
$oPage->DisplayForgotPwdForm();
$oPage->output();
exit;
}
else if ($operation == 'forgot_pwd_go')
{
$oPage = self::NewLoginWebPage();
$oPage->ForgotPwdGo();
$oPage->output();
exit;
}
else if ($operation == 'reset_pwd')
{
$oPage = self::NewLoginWebPage();
$oPage->DisplayResetPwdForm();
$oPage->output();
exit;
}
else if ($operation == 'do_reset_pwd')
{
$oPage = self::NewLoginWebPage();
$oPage->DoResetPassword();
$oPage->output();
exit;
}
else if ($operation == 'change_pwd')
{
$sAuthUser = $_SESSION['auth_user'];
UserRights::Login($sAuthUser); // Set the user's language
$oPage = new LoginWebPage();
$oPage = self::NewLoginWebPage();
$oPage->DisplayChangePwdForm();
$oPage->output();
exit;
@@ -424,30 +760,43 @@ EOF
$sNewPwd = utils::ReadPostedParam('new_pwd', '', false, 'raw_data');
if (UserRights::CanChangePassword() && ((!UserRights::CheckCredentials($sAuthUser, $sOldPwd)) || (!UserRights::ChangePassword($sOldPwd, $sNewPwd))))
{
$oPage = new LoginWebPage();
$oPage = self::NewLoginWebPage();
$oPage->DisplayChangePwdForm(true); // old pwd was wrong
$oPage->output();
exit;
}
$sMessage = Dict::S('UI:Login:PasswordChanged');
}
self::Login();
$iRet = self::Login($iOnExit);
if ($bMustBeAdmin && !UserRights::IsAdministrator())
{
require_once(APPROOT.'/setup/setuppage.class.inc.php');
$oP = new SetupPage(Dict::S('UI:PageTitle:FatalError'));
$oP->add("<h1>".Dict::S('UI:Login:Error:AccessAdmin')."</h1>\n");
$oP->p("<a href=\"".utils::GetAbsoluteUrlAppRoot()."pages/logoff.php\">".Dict::S('UI:LogOffMenu')."</a>");
$oP->output();
exit;
}
elseif ( (!$bIsAllowedToPortalUsers) && (UserRights::IsPortalUser()))
if ($iRet == self::EXIT_CODE_OK)
{
// No rights to be here, redirect to the portal
header('Location: '.utils::GetAbsoluteUrlAppRoot().'portal/index.php');
if ($bMustBeAdmin && !UserRights::IsAdministrator())
{
if ($iOnExit == self::EXIT_RETURN)
{
return self::EXIT_CODE_MUSTBEADMIN;
}
else
{
require_once(APPROOT.'/setup/setuppage.class.inc.php');
$oP = new SetupPage(Dict::S('UI:PageTitle:FatalError'));
$oP->add("<h1>".Dict::S('UI:Login:Error:AccessAdmin')."</h1>\n");
$oP->p("<a href=\"".utils::GetAbsoluteUrlAppRoot()."pages/logoff.php\">".Dict::S('UI:LogOffMenu')."</a>");
$oP->output();
exit;
}
}
$iRet = call_user_func(array(self::$sHandlerClass, 'ChangeLocation'), $bIsAllowedToPortalUsers, $iOnExit);
}
if ($iOnExit == self::EXIT_RETURN)
{
return $iRet;
}
else
{
return $sMessage;
}
return $sMessage;
}
} // End of class
?>

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -184,8 +184,9 @@ class ApplicationMenu
$oPage->AddToMenu('</ul>');
if ($bActive)
{
$oPage->add_ready_script("$('#accordion').accordion('activate', $iAccordion);");
$oPage->add_ready_script("$('#accordion').accordion('option', {collapsible: true});"); // Make it auto-collapsible once it has been opened properly
//$oPage->add_ready_script("$('#accordion').accordion('activate', $iAccordion);");
// $oPage->add_ready_script("$('#accordion').accordion('option', {collapsible: true});"); // Make it auto-collapsible once it has been opened properly
$oPage->add_ready_script("$('#accordion').accordion('option', {collapsible: true, active: $iAccordion});"); // Make it auto-collapsible once it has been opened properly
}
}
$oPage->AddToMenu('</div>');
@@ -299,7 +300,9 @@ class ApplicationMenu
// Make sure the root menu is sorted on 'rank'
usort(self::$aRootMenus, array('ApplicationMenu', 'CompareOnRank'));
$oFirstGroup = self::GetMenuNode(self::$aRootMenus[0]['index']);
$oMenuNode = self::GetMenuNode(self::$aMenusIndex[$oFirstGroup->GetIndex()]['children'][0]['index']);
$aChildren = self::$aMenusIndex[$oFirstGroup->GetIndex()]['children'];
usort($aChildren, array('ApplicationMenu', 'CompareOnRank'));
$oMenuNode = self::GetMenuNode($aChildren[0]['index']);
$sMenuId = $oMenuNode->GetMenuId();
}
return $sMenuId;
@@ -876,8 +879,41 @@ class DashboardMenuNode extends MenuNode
$oDashboard = $this->GetDashboard();
if ($oDashboard != null)
{
$sDivId = preg_replace('/[^a-zA-Z0-9_]/', '', $this->sMenuId);
$oPage->add('<div class="dashboard_contents" id="'.$sDivId.'">');
$oDashboard->Render($oPage, false, $aExtraParams);
$oPage->add('</div>');
$oDashboard->RenderEditionTools($oPage);
if ($oDashboard->GetAutoReload())
{
$sId = $this->sMenuId;
$sExtraParams = json_encode($aExtraParams);
$iReloadInterval = 1000 * $oDashboard->GetAutoReloadInterval();
$oPage->add_script(
<<<EOF
setInterval("ReloadDashboard('$sDivId');", $iReloadInterval);
function ReloadDashboard(sDivId)
{
var oExtraParams = $sExtraParams;
// Do not reload when a dialog box is active
if (!($('.ui-dialog:visible').length > 0))
{
$('.dashboard_contents#'+sDivId).block();
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
{ operation: 'reload_dashboard', dashboard_id: '$sId', extra_params: oExtraParams},
function(data){
$('.dashboard_contents#'+sDivId).html(data);
$('.dashboard_contents#'+sDivId).unblock();
}
);
}
}
EOF
);
}
$bEdit = utils::ReadParam('edit', false);
if ($bEdit)
{

View File

@@ -37,9 +37,10 @@ class NiceWebPage extends WebPage
{
parent::__construct($s_title);
$this->m_aReadyScripts = array();
$this->add_linked_script("../js/jquery-1.7.1.min.js");
$this->add_linked_stylesheet('../css/ui-lightness/jquery-ui-1.8.17.custom.css');
$this->add_linked_script('../js/jquery-ui-1.8.17.custom.min.js');
$this->add_linked_script("../js/jquery-1.10.0.min.js");
$this->add_linked_script("../js/jquery-migrate-1.2.1.min.js"); // Needed since many other plugins still rely on oldies like $.browser
$this->add_linked_stylesheet('../css/ui-lightness/jquery-ui-1.10.3.custom.min.css');
$this->add_linked_script('../js/jquery-ui-1.10.3.custom.min.js');
$this->add_linked_script("../js/hovertip.js");
// table sorting
$this->add_linked_script("../js/jquery.tablesorter.js");
@@ -98,6 +99,7 @@ EOF
$this->m_sRootUrl = $this->GetAbsoluteUrlAppRoot();
$sAbsURLAppRoot = addslashes($this->m_sRootUrl);
$sAbsURLModulesRoot = addslashes($this->GetAbsoluteUrlModulesRoot());
$sEnvironment = addslashes(utils::GetCurrentEnvironment());
$sAppContext = addslashes($this->GetApplicationContext());
@@ -112,6 +114,23 @@ function GetAbsoluteUrlModulesRoot()
{
return '$sAbsURLModulesRoot';
}
function GetAbsoluteUrlModulePage(sModule, sPage, aArguments)
{
// aArguments is optional, it default to an empty hash
aArguments = typeof aArguments !== 'undefined' ? aArguments : {};
var sUrl = '$sAbsURLAppRoot'+'pages/exec.php?exec_module='+sModule+'&exec_page='+sPage+'&exec_env='+'$sEnvironment';
for (var sArgName in aArguments)
{
if (aArguments.hasOwnProperty(sArgName))
{
sUrl = sUrl + '&'+sArgName+'='+aArguments[sArgname];
}
}
return sUrl;
}
function AddAppContext(sURL)
{
var sContext = '$sAppContext';
@@ -200,7 +219,7 @@ EOF
*/
public function output()
{
$this->set_base($this->m_sRootUrl.'pages/');
//$this->set_base($this->m_sRootUrl.'pages/');
if (count($this->m_aReadyScripts)>0)
{
$this->add_script("\$(document).ready(function() {\n".implode("\n", $this->m_aReadyScripts)."\n});");

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -19,7 +19,7 @@
/**
* Class PortalWebPage
*
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @copyright Copyright (C) 2010-2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -64,6 +64,7 @@ class PortalWebPage extends NiceWebPage
$sAbsURLModulesRoot = addslashes(utils::GetAbsoluteUrlModulesRoot()); // Pass it to Javascript scripts
$oAppContext = new ApplicationContext();
$sAppContext = addslashes($oAppContext->GetForLink());
$this->add_dict_entry('UI:FillAllMandatoryFields');
if ($sAlternateStyleSheet != '')
{
$this->add_linked_stylesheet("../portal/$sAlternateStyleSheet/portal.css");
@@ -138,6 +139,17 @@ try
changeMonth: true,
changeYear: true
});
$(".datetime-pick").datepicker({
showOn: 'button',
buttonImage: '../images/calendar.png',
buttonImageOnly: true,
dateFormat: 'yy-mm-dd 00:00:00',
constrainInput: false,
changeMonth: true,
changeYear: true
});
//$('.resizable').resizable(); // Make resizable everything that claims to be resizable !
$('.caselog_header').click( function () { $(this).toggleClass('open').next('.caselog_entry').toggle(); });
}
@@ -224,6 +236,17 @@ EOF
// For Wizard helper to process the ajax replies
$this->add('<div id="ajax_content"></div>');
// Customize the logo (unless a customer CSS has been defined)
if ($sAlternateStyleSheet == '')
{
if (file_exists(MODULESROOT.'branding/portal-logo.png'))
{
$sDisplayIcon = utils::GetAbsoluteUrlModulesRoot().'branding/portal-logo.png';
$this->add_style("div#portal #logo {background: url(\"$sDisplayIcon\") no-repeat scroll 0 0 transparent;}");
}
}
}
public function SetCurrentTab($sTabLabel = '')
@@ -301,7 +324,7 @@ EOF
$sUniqueId = $sClass.$this->GetUniqueId();
$this->add("<div id=\"$sUniqueId\">\n"); // The id here MUST be the same as currentId, otherwise the pagination will be broken
cmdbAbstractObject::DisplaySet($this, $oSet, array('currentId' => $sUniqueId, 'menu' => false, 'zlist' => false, 'extra_fields' => implode(',', $aZList)));
cmdbAbstractObject::DisplaySet($this, $oSet, array('currentId' => $sUniqueId, 'menu' => false, 'toolkit_menu' => false, 'zlist' => false, 'extra_fields' => implode(',', $aZList)));
$this->add("</div>\n");
}
else
@@ -397,7 +420,7 @@ EOF
}
$oObjSearch->AddCondition_ReferencedBy($oLinkSet->GetFilter(), $sRemoteAttCode);
$aExtraParams = array('menu' => false, 'zlist' => false, 'extra_fields' => implode(',', $aZList));
$aExtraParams = array('menu' => false, 'toolkit_menu' => false, 'zlist' => false, 'extra_fields' => implode(',', $aZList));
$oBlock = new DisplayBlock($oObjSearch, 'list', false);
$oBlock->Display($this, 1, $aExtraParams);
}
@@ -407,8 +430,7 @@ EOF
}
}
protected function DisplaySearchField($sClass, $sAttSpec, $aExtraParams, $sPrefix, $sFieldName = null)
protected function DisplaySearchField($sClass, $sAttSpec, $aExtraParams, $sPrefix, $sFieldName = null, $aFilterParams = array())
{
if (is_null($sFieldName))
{
@@ -439,7 +461,7 @@ EOF
{
throw new Exception("Attribute specification '$sAttSpec', '$sAttCode' should be either a link set or an external key");
}
$this->DisplaySearchField($sTargetClass, $sSubSpec, $aExtraParams, $sPrefix, $sFieldName);
$this->DisplaySearchField($sTargetClass, $sSubSpec, $aExtraParams, $sPrefix, $sFieldName, $aFilterParams);
}
else
{
@@ -453,7 +475,24 @@ EOF
if ($oAttDef->IsExternalKey())
{
$sTargetClass = $oAttDef->GetTargetClass();
$oAllowedValues = new DBObjectSet(new DBObjectSearch($sTargetClass));
$sFilterDefName = 'PORTAL_TICKETS_SEARCH_FILTER_'.$sAttSpec;
if (defined($sFilterDefName))
{
try
{
$oFitlerWithParams = DBObjectSearch::FromOQL(constant($sFilterDefName));
$sFilterOQL = $oFitlerWithParams->ToOQL(true, $aFilterParams);
$oAllowedValues = new DBObjectSet(DBObjectSearch::FromOQL($sFilterOQL), array(), $aFilterParams);
}
catch(OQLException $e)
{
throw new Exception("Incorrect filter '$sFilterDefName' for attribute '$sAttcode': ".$e->getMessage());
}
}
else
{
$oAllowedValues = new DBObjectSet(new DBObjectSearch($sTargetClass));
}
$iFieldSize = $oAttDef->GetMaxSize();
$iMaxComboLength = $oAttDef->GetMaximumComboLength();
@@ -473,7 +512,8 @@ EOF
if (is_null($aAllowedValues))
{
// Any value is possible, display an input box
$this->add("<label>".MetaModel::GetFilterLabel($sClass, $sAttSpec).":</label>&nbsp;<input class=\"textSearch\" name=\"$sPrefix$sFieldName\" value=\"$sFilterValue\"/>\n");
$sSanitizedValue = htmlentities($sFilterValue, ENT_QUOTES, 'UTF-8');
$this->add("<label>".MetaModel::GetFilterLabel($sClass, $sAttSpec).":</label>&nbsp;<input class=\"textSearch\" name=\"$sPrefix$sFieldName\" value=\"$sSanitizedValue\"/>\n");
}
else
{
@@ -510,9 +550,30 @@ EOF
}
}
/**
* Get The organization of the current user (i.e. the organization of its contact)
* @throws Exception
*/
function GetUserOrg()
{
$oOrg = null;
$iContactId = UserRights::GetContactId();
$oContact = MetaModel::GetObject('Contact', $iContactId, false); // false => Can fail
if (is_object($oContact))
{
$oOrg = MetaModel::GetObject('Organization', $oContact->Get('org_id'), false); // false => can fail
}
else
{
throw new Exception(Dict::S('Portal:ErrorNoContactForThisUser'));
}
return $oOrg;
}
public function DisplaySearchForm($sClass, $aAttList, $aExtraParams, $sPrefix, $bClosed = true)
{
$oUserOrg = $this->GetUserOrg();
$aFilterParams = array('org_id' => $oUserOrg->GetKey(), 'contact_id' => UserRights::GetContactId());
$sCSSClass = ($bClosed) ? 'DrawerClosed' : '';
$this->add("<div id=\"ds_$sPrefix\" class=\"SearchDrawer $sCSSClass\">\n");
$this->add_ready_script(
@@ -529,13 +590,17 @@ EOF
foreach($aAttList as $sAttSpec)
{
//$oAppContext->Reset($sAttSpec); // Make sure the same parameter will not be passed twice
$this->DisplaySearchField($sClass, $sAttSpec, $aExtraParams, $sPrefix);
$this->DisplaySearchField($sClass, $sAttSpec, $aExtraParams, $sPrefix, null, $aFilterParams);
}
$this->add("</p>\n");
$this->add("<p align=\"right\"><input type=\"submit\" value=\"".Dict::S('UI:Button:Search')."\"></p>\n");
foreach($aExtraParams as $sName => $sValue)
{
$this->add("<input type=\"hidden\" name=\"$sName\" value=\"$sValue\" />\n");
// Note: use DumpHiddenParams() to transmit arrays as hidden params
if (is_scalar($sValue))
{
$this->add("<input type=\"hidden\" name=\"$sName\" value=\"$sValue\" />\n");
}
}
// $this->add($oAppContext->GetForForm());
$this->add("</form>\n");
@@ -731,7 +796,7 @@ EOF
}
}
$oObj = MetaModel::GetObject($sClass, $iId, false);
$oObj = MetaModel::GetObject($sClass, $iId, false);
if (!is_object($oObj))
{
throw new Exception("Could not find the object $sClass/$iId");
@@ -784,7 +849,10 @@ EOF
}
if ($iButtonFlags & BUTTON_BACK)
{
$aButtons[] = "<input id=\"btn_back\" type=\"submit\" value=\"".Dict::S('UI:Button:Back')."\" onClick=\"GoBack('{$this->m_sWizardId}');\">";
if (utils::ReadParam('step_back', 1) != 1)
{
$aButtons[] = "<input id=\"btn_back\" type=\"submit\" value=\"".Dict::S('UI:Button:Back')."\" onClick=\"GoBack('{$this->m_sWizardId}');\">";
}
}
if ($iButtonFlags & BUTTON_NEXT)
{

View File

@@ -47,13 +47,13 @@ abstract class Query extends cmdbAbstractObject
MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeText("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeString("fields", array("allowed_values"=>null, "sql"=>"fields", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeText("fields", array("allowed_values"=>null, "sql"=>"fields", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
// Display lists
MetaModel::Init_SetZListItems('details', array('name', 'description', 'fields')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list
// Search criteria
// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
MetaModel::Init_SetZListItems('standard_search', array('name', 'description', 'fields')); // Criteria of the std search form
// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
}
}
@@ -82,26 +82,48 @@ class QueryOQL extends Query
MetaModel::Init_SetZListItems('details', array('name', 'description', 'oql', 'fields')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list
// Search criteria
// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
MetaModel::Init_SetZListItems('standard_search', array('name', 'description', 'fields', 'oql')); // Criteria of the std search form
// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
}
function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array())
{
parent::DisplayBareProperties($oPage, $bEditMode, $sPrefix, $aExtraParams);
$aFieldsMap = parent::DisplayBareProperties($oPage, $bEditMode, $sPrefix, $aExtraParams);
if (!$bEditMode)
{
$sUrl = utils::GetAbsoluteUrlAppRoot().'webservices/export.php?format=spreadsheet&login_mode=basic&query='.$this->GetKey();
$sOql = $this->Get('oql');
$oSearch = DBObjectSearch::FromOQL($sOql);
$aParameters = $oSearch->GetQueryParams();
foreach($aParameters as $sParam => $val)
$sMessage = null;
try
{
$sUrl .= '&arg_'.$sParam.'=["'.$sParam.'"]';
$oSearch = DBObjectSearch::FromOQL($sOql);
$aParameters = $oSearch->GetQueryParams();
foreach($aParameters as $sParam => $val)
{
$sUrl .= '&arg_'.$sParam.'=["'.$sParam.'"]';
}
$oPage->p(Dict::S('UI:Query:UrlForExcel').':<br/><textarea cols="80" rows="3" READONLY>'.$sUrl.'</textarea>');
if (count($aParameters) == 0)
{
$oBlock = new DisplayBlock($oSearch, 'list');
$aExtraParams = array(
//'menu' => $sShowMenu,
'table_id' => 'query_preview_'.$this->getKey(),
);
$sBlockId = 'block_query_preview_'.$this->GetKey(); // make a unique id (edition occuring in the same DOM)
$oBlock->Display($oPage, $sBlockId, $aExtraParams);
}
}
catch (OQLException $e)
{
$sMessage = '<div class="message message_error" style="padding-left: 30px;"><div style="padding: 10px;">'.Dict::Format('UI:RunQuery:Error', $e->getHtmlDesc()).'</div></div>';
$oPage->p($sMessage);
}
$oPage->p(Dict::S('UI:Query:UrlForExcel').':<br/><textarea cols="80" rows="3" READONLY>'.$sUrl.'</textarea>');
}
return $aFieldsMap;
}
}

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -142,6 +142,7 @@ EOF
function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array())
{
return array();
}
// End of the minimal implementation of iDisplay
}
@@ -165,6 +166,8 @@ class ShortcutOQL extends Shortcut
MetaModel::Init_Params($aParams);
MetaModel::Init_InheritAttributes();
MetaModel::Init_AddAttribute(new AttributeOQL("oql", array("allowed_values"=>null, "sql"=>"oql", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeEnum("auto_reload", array("allowed_values"=>new ValueSetEnum('none,custom'), "sql"=>"auto_reload", "default_value"=>"none", "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeInteger("auto_reload_sec", array("allowed_values"=>null, "sql"=>"auto_reload_sec", "default_value"=>60, "is_null_allowed"=>false, "depends_on"=>array())));
// Display lists
MetaModel::Init_SetZListItems('details', array('name', 'context', 'oql')); // Attributes to be displayed for the complete details
@@ -178,6 +181,21 @@ class ShortcutOQL extends Shortcut
{
$oPage->set_title($this->Get('name'));
switch($this->Get('auto_reload'))
{
case 'custom':
$iRate = (int)$this->Get('auto_reload_sec');
if ($iRate > 0)
{
// Must a string otherwise it can be evaluated to 'true' and defaults to "standard" refresh rate!
$aExtraParams['auto_reload'] = (string)$iRate;
}
break;
default:
case 'none':
}
$bSearchPane = true;
$bSearchOpen = false;
try
@@ -191,7 +209,18 @@ class ShortcutOQL extends Shortcut
}
public static function GetCreationForm($sOQL = null)
public function CloneTableSettings($sTableSettings)
{
$aTableSettings = json_decode($sTableSettings, true);
$oFilter = DBObjectSearch::FromOQL($this->Get('oql'));
$oCustomSettings = new DataTableSettings($oFilter->GetSelectedClasses());
$oCustomSettings->iDefaultPageSize = $aTableSettings['iPageSize'];
$oCustomSettings->aColumns = $aTableSettings['oColumns'];
$oCustomSettings->Save('shortcut_'.$this->GetKey());
}
public static function GetCreationForm($sOQL = null, $sTableSettings = null)
{
$oForm = new DesignerForm();
@@ -214,20 +243,36 @@ class ShortcutOQL extends Shortcut
$oField = new DesignerTextField('name', Dict::S('Class:Shortcut/Attribute:name'), $sDefault);
$oField->SetMandatory(true);
$oForm->AddField($oField);
//$oField = new DesignerLongTextField('oql', Dict::S('Class:Shortcut/Attribute:oql'), $sOQL);
//$oField->SetMandatory();
/*
$oField = new DesignerComboField('auto_reload', Dict::S('Class:ShortcutOQL/Attribute:auto_reload'), 'none');
$oAttDef = MetaModel::GetAttributeDef(__class__, 'auto_reload');
$oField->SetAllowedValues($oAttDef->GetAllowedValues());
$oField->SetMandatory(true);
$oForm->AddField($oField);
*/
$oField = new DesignerBooleanField('auto_reload', Dict::S('Class:ShortcutOQL/Attribute:auto_reload'), false);
$oForm->AddField($oField);
$oField = new DesignerTextField('auto_reload_sec', Dict::S('Class:ShortcutOQL/Attribute:auto_reload_sec'), MetaModel::GetConfig()->GetStandardReloadInterval());
$oField->SetValidationPattern('^$|^0*([5-9]|[1-9][0-9]+)$'); // Can be empty, or a number > 4
$oField->SetMandatory(false);
$oForm->AddField($oField);
$oField = new DesignerHiddenField('oql', '', $sOQL);
$oForm->AddField($oField);
$oField = new DesignerHiddenField('table_settings', '', $sTableSettings);
$oForm->AddField($oField);
return $oForm;
}
public static function GetCreationDlgFromOQL($oPage, $sOQL)
public static function GetCreationDlgFromOQL($oPage, $sOQL, $sTableSettings)
{
$oPage->add('<div id="shortcut_creation_dlg">');
$oForm = self::GetCreationForm($sOQL);
$oForm = self::GetCreationForm($sOQL, $sTableSettings);
$oForm->Render($oPage);
$oPage->add('</div>');
@@ -239,9 +284,19 @@ class ShortcutOQL extends Shortcut
$oAppContext = new ApplicationContext();
$sContext = $oAppContext->GetForLink();
$sRateTitle = addslashes(Dict::S('Class:ShortcutOQL/Attribute:auto_reload_sec+'));
$oPage->add_ready_script(
<<<EOF
// Note: the title gets deleted by the validation mechanism
$("#attr_auto_reload_sec").tooltip({items: 'input', content: '$sRateTitle'});
$("#attr_auto_reload_sec").prop('disabled', !$('#attr_auto_reload').is(':checked'));
$('#attr_auto_reload').change( function(ev) {
$("#attr_auto_reload_sec").prop('disabled', !$(this).is(':checked'));
} );
function ShortcutCreationOK()
{
var oForm = $('#shortcut_creation_dlg form');

View File

@@ -283,9 +283,29 @@ class ObjectDetailsTemplate extends DisplayTemplate
$sStateAttCode = MetaModel :: GetStateAttributeCode(get_class($this->m_oObj));
$aTemplateFields = array();
preg_match_all('/\\$this->([a-z0-9_]+)\\$/', $this->m_sTemplate, $aMatches);
$aTemplateFields = $aMatches[1];
foreach ($aMatches[1] as $sAttCode)
{
if (MetaModel::IsValidAttCode(get_class($this->m_oObj), $sAttCode))
{
$aTemplateFields[] = $sAttCode;
}
else
{
$aParams['this->'.$sAttCode] = "<!--Unknown attribute: $sAttCode-->";
}
}
preg_match_all('/\\$this->field\\(([a-z0-9_]+)\\)\\$/', $this->m_sTemplate, $aMatches);
$aTemplateFields = array_merge($aTemplateFields, $aMatches[1]);
foreach ($aMatches[1] as $sAttCode)
{
if (MetaModel::IsValidAttCode(get_class($this->m_oObj), $sAttCode))
{
$aTemplateFields[] = $sAttCode;
}
else
{
$aParams['this->field('.$sAttCode.')'] = "<!--Unknown attribute: $sAttCode-->";
}
}
$aFieldsComments = (isset($aParams['fieldsComments'])) ? $aParams['fieldsComments'] : array();
$aFieldsMap = array();

View File

@@ -1,20 +0,0 @@
<div class="page_header" style="padding:0.5em;">
<h1><itopstring>UI:NotificationsMenu:Title</itopstring></h1>
</div>
<itoptoggle name="UI:NotificationsMenu:Help" open="true">
<div style="padding: 1em; font-size:10pt;background:#E8F3CF;margin-top: 0.25em;">
<img src="../images/bell.png" style="margin-top: -60px; margin-right: 10px; float: right;">
<itopstring>UI:NotificationsMenu:HelpContent</itopstring>
</div>
</itoptoggle>
<p>&nbsp;</p>
<itoptabs>
<itoptab name="UI:NotificationsMenu:Triggers">
<h2><itopstring>UI:NotificationsMenu:AvailableTriggers</itopstring></h2>
<itopblock BlockClass="DisplayBlock" type="list" asynchronous="false" encoding="text/oql">SELECT Trigger</itopblock>
</itoptab>
<itoptab name="UI:NotificationsMenu:Actions">
<h2><itopstring>UI:NotificationsMenu:AvailableActions</itopstring></h2>
<itopblock BlockClass="DisplayBlock" type="list" asynchronous="false" encoding="text/oql">SELECT ActionEmail</itopblock>
</itoptab>
</itoptabs>

View File

@@ -96,7 +96,7 @@ class UIExtKeyWidget
}
/**
* Get the HTML fragment corresponding to the linkset editing widget
* Get the HTML fragment corresponding to the ext key editing widget
* @param WebPage $oP The web page used for all the output
* @param Hash $aArgs Extra context arguments
* @return string The HTML fragment to be inserted into the page
@@ -207,7 +207,16 @@ class UIExtKeyWidget
$sHTMLValue .= "</select>\n";
if (($this->bSearchMode) && $bSearchMultiple)
{
$oPage->add_ready_script("$('.multiselect').multiselect({header: false, noneSelectedText: '".addslashes(Dict::S('UI:SearchValue:Any'))."', selectedList: 1, selectedText:'".addslashes(Dict::S('UI:SearchValue:NbSelected'))."'});");
$aOptions = array(
'header' => true,
'checkAllText' => Dict::S('UI:SearchValue:CheckAll'),
'uncheckAllText' => Dict::S('UI:SearchValue:UncheckAll'),
'noneSelectedText' => Dict::S('UI:SearchValue:Any'),
'selectedText' => Dict::S('UI:SearchValue:NbSelected'),
'selectedList' => 1,
);
$sJSOptions = json_encode($aOptions);
$oPage->add_ready_script("$('.multiselect').multiselect($sJSOptions);");
}
$oPage->add_ready_script(
<<<EOF
@@ -225,6 +234,15 @@ EOF
// Too many choices, use an autocomplete
$sSelectMode = 'false';
// Check that the given value is allowed
$oSearch = $oAllowedValues->GetFilter();
$oSearch->AddCondition('id', $value);
$oSet = new DBObjectSet($oSearch);
if ($oSet->Count() == 0)
{
$value = null;
}
if (is_null($value) || ($value == 0)) // Null values are displayed as ''
{
$sDisplayValue = isset($aArgs['sDefaultValue']) ? $aArgs['sDefaultValue'] : '';
@@ -299,8 +317,9 @@ EOF
if ( ($oCurrObject != null) && ($this->sAttCode != ''))
{
$oAttDef = MetaModel::GetAttributeDef(get_class($oCurrObject), $this->sAttCode);
$aParams = array('query_params' => array('this' => $oCurrObject));
$oSet = $oAttDef->GetAllowedValuesAsObjectSet($aParams);
$aArgs = array('this' => $oCurrObject);
$aParams = array('query_params' => $aArgs);
$oSet = $oAttDef->GetAllowedValuesAsObjectSet($aArgs);
$oFilter = $oSet->GetFilter();
}
else
@@ -346,6 +365,10 @@ EOF
}
$oFilter = DBObjectSearch::FromOQL($sFilter);
if (strlen($sRemoteClass) > 0)
{
$oFilter->ChangeClass($sRemoteClass);
}
$oFilter->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode);
$oBlock = new DisplayBlock($oFilter, 'list', false, array('query_params' => array('this' => $oObj)));
$oBlock->Display($oP, $this->iId.'_results', array('this' => $oObj, 'cssCount'=> '#count_'.$this->iId, 'menu' => false, 'selection_mode' => true, 'selection_type' => 'single', 'table_id' => 'select_'.$this->sAttCode)); // Don't display the 'Actions' menu on the results
@@ -430,7 +453,7 @@ EOF
cmdbAbstractObject::DisplayCreationForm($oPage, $this->sTargetClass, $oNewObj, array(), array('formPrefix' => $this->iId, 'noRelations' => true));
$oPage->add('</div></div></div>');
// $oPage->add_ready_script("\$('#ac_create_$this->iId').dialog({ width: $(window).width()*0.8, height: 'auto', autoOpen: false, modal: true, title: '$sDialogTitle'});\n");
$oPage->add_ready_script("\$('#ac_create_$this->iId').dialog({ width: 'auto', height: 'auto', autoOpen: false, modal: true, title: '$sDialogTitle'});\n");
$oPage->add_ready_script("\$('#ac_create_$this->iId').dialog({ width: 'auto', height: 'auto', maxHeight: $(window).height() - 50, autoOpen: false, modal: true, title: '$sDialogTitle'});\n");
$oPage->add_ready_script("$('#dcr_{$this->iId} form').removeAttr('onsubmit');");
$oPage->add_ready_script("$('#dcr_{$this->iId} form').bind('submit.uilinksWizard', oACWidget_{$this->iId}.DoCreateObject);");
}
@@ -534,6 +557,7 @@ EOF
$aSortedRoots = $aTree[$iRootId];
asort($aSortedRoots);
$oP->add("<ul>\n");
$fUniqueId = microtime(true);
foreach($aSortedRoots as $id => $sName)
{
if ($bSelect)
@@ -541,14 +565,14 @@ EOF
$sChecked = ($aNodes[$id]->GetKey() == $currValue) ? 'checked' : '';
if ($bMultiple)
{
$sSelect = '<input type="checkbox" value="'.$aNodes[$id]->GetKey().'" name="selectObject[]" '.$sChecked.'>&nbsp;';
$sSelect = '<input id="input_'.$fUniqueId.'_'.$aNodes[$id]->GetKey().'" type="checkbox" value="'.$aNodes[$id]->GetKey().'" name="selectObject[]" '.$sChecked.'>&nbsp;';
}
else
{
$sSelect = '<input type="radio" value="'.$aNodes[$id]->GetKey().'" name="selectObject" '.$sChecked.'>&nbsp;';
$sSelect = '<input id="input_'.$fUniqueId.'_'.$aNodes[$id]->GetKey().'" type="radio" value="'.$aNodes[$id]->GetKey().'" name="selectObject" '.$sChecked.'>&nbsp;';
}
}
$oP->add('<li>'.$sSelect.$aNodes[$id]->GetHyperlink());
$oP->add('<li>'.$sSelect.'<label for="input_'.$fUniqueId.'_'.$aNodes[$id]->GetKey().'">'.$aNodes[$id]->GetName().'</label>');
$this->DumpNodes($oP, $id, $aTree, $aNodes, $currValue);
$oP->add("</li>\n");
}

View File

@@ -46,14 +46,24 @@ class UILinksWidgetDirect
$oLinksetDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
$this->sLinkedClass = $oLinksetDef->GetLinkedClass();
$sExtKeyToMe = $oLinksetDef->GetExtKeyToMe();
$aZList = MetaModel::FlattenZList(MetaModel::GetZListItems($this->sLinkedClass, 'list'));
switch($oLinksetDef->GetEditMode())
{
case LINKSET_EDITMODE_INPLACE: // The whole linkset can be edited 'in-place'
$aZList = MetaModel::FlattenZList(MetaModel::GetZListItems($this->sLinkedClass, 'details'));
break;
default:
$aZList = MetaModel::FlattenZList(MetaModel::GetZListItems($this->sLinkedClass, 'list'));
array_unshift($aZList, 'friendlyname');
}
foreach($aZList as $sLinkedAttCode)
{
if ($sLinkedAttCode != $sExtKeyToMe)
{
$oAttDef = MetaModel::GetAttributeDef($this->sLinkedClass, $sLinkedAttCode);
if (!$oAttDef->IsExternalField() || ($oAttDef->GetKeyAttCode() != $sExtKeyToMe) )
if ((!$oAttDef->IsExternalField() || ($oAttDef->GetKeyAttCode() != $sExtKeyToMe)) &&
(!$oAttDef->IsLinkSet()) )
{
$this->aZlist[] = $sLinkedAttCode;
}
@@ -88,6 +98,18 @@ class UILinksWidgetDirect
$this->DisplayEditInPlace($oPage, $oValue, $aArgs, $sFormPrefix, $oCurrentObj);
break;
case LINKSET_EDITMODE_ADDREMOVE: // The whole linkset can be edited 'in-place'
$sTargetClass = $oLinksetDef->GetLinkedClass();
$sExtKeyToMe = $oLinksetDef->GetExtKeyToMe();
$oExtKeyDef = MetaModel::GetAttributeDef($sTargetClass, $sExtKeyToMe);
$aButtons = array('add');
if ($oExtKeyDef->IsNullAllowed())
{
$aButtons = array('add', 'remove');
}
$this->DisplayEditInPlace($oPage, $oValue, $aArgs, $sFormPrefix, $oCurrentObj, $aButtons);
break;
case LINKSET_EDITMODE_ACTIONS:
default:
$this->DisplayAsBlock($oPage, $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj, true /* bDisplayMenu*/);
@@ -130,7 +152,7 @@ class UILinksWidgetDirect
}
}
protected function DisplayEditInPlace(WebPage $oPage, DBObjectSet $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj)
protected function DisplayEditInPlace(WebPage $oPage, DBObjectSet $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj, $aButtons = array('create', 'delete'))
{
$aAttribs = $this->GetTableConfig();
@@ -156,10 +178,16 @@ class UILinksWidgetDirect
// 'modify' => 'Modify...' ,
'creation_title' => Dict::Format('UI:CreationTitle_Class', MetaModel::GetName($this->sLinkedClass)),
'create' => Dict::Format('UI:ClickToCreateNew', MetaModel::GetName($this->sLinkedClass)),
'remove' => Dict::S('UI:Button:Remove'),
'add' => Dict::Format('UI:AddAnExisting_Class', MetaModel::GetName($this->sLinkedClass)),
'selection_title' => Dict::Format('UI:SelectionOf_Class', MetaModel::GetName($this->sLinkedClass)),
);
$sSubmitUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
$oContext = new ApplicationContext();
$sSubmitUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?'.$oContext->GetForLink();
$sJSONLabels = json_encode($aLabels);
$oPage->add_ready_script("$('#{$this->sInputid}').directlinks({class_name: '$this->sClass', att_code: '$this->sAttCode', input_name:'$sInputName', labels: $sJSONLabels, sumit_to: '$sSubmitUrl' });");
$sJSONButtons = json_encode($aButtons);
$sWizHelper = 'oWizardHelper'.$sFormPrefix;
$oPage->add_ready_script("$('#{$this->sInputid}').directlinks({class_name: '$this->sClass', att_code: '$this->sAttCode', input_name:'$sInputName', labels: $sJSONLabels, submit_to: '$sSubmitUrl', buttons: $sJSONButtons, oWizardHelper: $sWizHelper });");
}
public function GetObjectCreationDlg(WebPage $oPage, $sProposedRealClass = '')
@@ -212,6 +240,103 @@ class UILinksWidgetDirect
$oPage->add('</div></div>');
}
public function GetObjectsSelectionDlg($oPage, $oCurrentObj)
{
$sHtml = "<div class=\"wizContainer\" style=\"vertical-align:top;\">\n";
$oLinksetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode);
$valuesDef = $oLinksetDef->GetValuesDef();
if ($valuesDef === null)
{
$oFilter = new DBObjectSearch($this->sLinkedClass);
}
else
{
if (!$valuesDef instanceof ValueSetObjects)
{
throw new Exception('Error: only ValueSetObjects are supported for "allowed_values" in AttributeLinkedSet ('.$this->sClass.'/'.$this->sAttCode.').');
}
$oFilter = DBObjectSearch::FromOQL($valuesDef->GetFilterExpression());
}
if ($oCurrentObj != null)
{
$this->SetSearchDefaultFromContext($oCurrentObj, $oFilter);
}
$oBlock = new DisplayBlock($oFilter, 'search', false);
$sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->sInputid}", array('open' => true));
$sHtml .= "<form id=\"ObjectsAddForm_{$this->sInputid}\">\n";
$sHtml .= "<div id=\"SearchResultsToAdd_{$this->sInputid}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n";
$sHtml .= "<div style=\"background: #fff; border:0; text-align:center; vertical-align:middle;\"><p>".Dict::S('UI:Message:EmptyList:UseSearchForm')."</p></div>\n";
$sHtml .= "</div>\n";
$sHtml .= "<input type=\"hidden\" id=\"count_{$this->sInputid}\" value=\"0\"/>";
$sHtml .= "<button type=\"button\" class=\"cancel\">".Dict::S('UI:Button:Cancel')."</button>&nbsp;&nbsp;<button type=\"button\" class=\"ok\" disabled=\"disabled\">".Dict::S('UI:Button:Add')."</button>";
$sHtml .= "</div>\n";
$sHtml .= "</form>\n";
$oPage->add($sHtml);
//$oPage->add_ready_script("$('#SearchFormToAdd_{$this->sAttCode}{$this->sNameSuffix} form').bind('submit.uilinksWizard', oWidget{$this->sInputId}.SearchObjectsToAdd);");
//$oPage->add_ready_script("$('#SearchFormToAdd_{$this->sAttCode}{$this->sNameSuffix}').resize(oWidget{$this->siInputId}.UpdateSizes);");
}
/**
* Search for objects to be linked to the current object (i.e "remote" objects)
* @param WebPage $oP The page used for the output (usually an AjaxWebPage)
* @param string $sRemoteClass Name of the "remote" class to perform the search on, must be a derived class of $this->sLinkedClass
* @param array $aAlreadyLinked Array of indentifiers of objects which are already linke to the current object (or about to be linked)
* @param DBObject $oCurrentObj The object currently being edited... if known...
*/
public function SearchObjectsToAdd(WebPage $oP, $sRemoteClass = '', $aAlreadyLinked = array(), $oCurrentObj = null)
{
if ($sRemoteClass == '')
{
$sRemoteClass = $this->sLinkedClass;
}
$oLinksetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode);
$valuesDef = $oLinksetDef->GetValuesDef();
if ($valuesDef === null)
{
$oFilter = new DBObjectSearch($this->sLinkedClass);
}
else
{
if (!$valuesDef instanceof ValueSetObjects)
{
throw new Exception('Error: only ValueSetObjects are supported for "allowed_values" in AttributeLinkedSet ('.$this->sClass.'/'.$this->sAttCode.').');
}
$oFilter = DBObjectSearch::FromOQL($valuesDef->GetFilterExpression());
}
if (($oCurrentObj != null) && MetaModel::IsSameFamilyBranch($sRemoteClass, $this->sClass))
{
// Prevent linking to self if the linked object is of the same family
// and laready present in the database
if (!$oCurrentObj->IsNew())
{
$oFilter->AddCondition('id', $oCurrentObj->GetKey(), '!=');
}
}
if (count($aAlreadyLinked) > 0)
{
$oFilter->AddCondition('id', $aAlreadyLinked, 'NOTIN');
}
if ($oCurrentObj != null)
{
$aArgs = array_merge($oCurrentObj->ToArgs('this'), $oFilter->GetInternalParams());
$oFilter->SetInternalParams($aArgs);
}
$oBlock = new DisplayBlock($oFilter, 'list', false);
$oBlock->Display($oP, "ResultsToAdd_{$this->sInputid}", array('menu' => false, 'cssCount'=> '#count_'.$this->sInputid , 'selection_mode' => true, 'table_id' => 'add_'.$this->sInputid)); // Don't display the 'Actions' menu on the results
}
public function DoAddObjects(WebPage $oP, $oFullSetFilter)
{
$aLinkedObjectIds = utils::ReadMultipleSelection($oFullSetFilter);
foreach($aLinkedObjectIds as $iObjectId)
{
$oLinkObj = MetaModel::GetObject($this->sLinkedClass, $iObjectId);
$oP->add($this->GetObjectRow($oP, $oLinkObj, $oLinkObj->GetKey()));
}
}
public function GetObjectModificationDlg()
{
@@ -229,9 +354,9 @@ class UILinksWidgetDirect
}
return $aAttribs;
}
public function GetRow($oPage, $sRealClass, $aValues, $iTempId)
{
$aAttribs = $this->GetTableConfig();
if ($sRealClass == '')
{
$sRealClass = $this->sLinkedClass;
@@ -239,17 +364,60 @@ class UILinksWidgetDirect
$oLinkObj = new $sRealClass();
$oLinkObj->UpdateObjectFromPostedForm($this->sInputid);
return $this->GetObjectRow($oPage, $oLinkObj, $iTempId);
}
protected function GetObjectRow($oPage, $oLinkObj, $iTempId)
{
$aAttribs = $this->GetTableConfig();
$aRow = array();
$aRow['form::select'] = '<input type="checkbox" class="selectList'.$this->sInputid.'" value="'.(-$iTempId).'"/>';
$aRow['form::select'] = '<input type="checkbox" class="selectList'.$this->sInputid.'" value="'.($iTempId).'"/>';
foreach($this->aZlist as $sLinkedAttCode)
{
$aRow[$sLinkedAttCode] = $oLinkObj->GetAsHTML($sLinkedAttCode);
}
return $oPage->GetTableRow($aRow, $aAttribs);
return $oPage->GetTableRow($aRow, $aAttribs);
}
public function UpdateFromArray($oObj, $aData)
/**
* Initializes the default search parameters based on 1) a 'current' object and 2) the silos defined by the context
* @param DBObject $oSourceObj
* @param DBObjectSearch $oSearch
*/
protected function SetSearchDefaultFromContext($oSourceObj, &$oSearch)
{
$oAppContext = new ApplicationContext();
$sSrcClass = get_class($oSourceObj);
$sDestClass = $oSearch->GetClass();
foreach($oAppContext->GetNames() as $key)
{
// Find the value of the object corresponding to each 'context' parameter
$aCallSpec = array($sSrcClass, 'MapContextParam');
$sAttCode = '';
if (is_callable($aCallSpec))
{
$sAttCode = call_user_func($aCallSpec, $key); // Returns null when there is no mapping for this parameter
}
if (MetaModel::IsValidAttCode($sSrcClass, $sAttCode))
{
$oAttDef = MetaModel::GetAttributeDef($sSrcClass, $sAttCode);
$defaultValue = $oSourceObj->Get($sAttCode);
// Find the attcode for the same 'context' parameter in the destination class
// and sets its value as the default value for the search condition
$aCallSpec = array($sDestClass, 'MapContextParam');
$sAttCode = '';
if (is_callable($aCallSpec))
{
$sAttCode = call_user_func($aCallSpec, $key); // Returns null when there is no mapping for this parameter
}
if (MetaModel::IsValidAttCode($sDestClass, $sAttCode) && !empty($defaultValue))
{
$oSearch->AddCondition($sAttCode, $defaultValue);
}
}
}
}
}

View File

@@ -106,7 +106,7 @@ class UILinksWidget
$sPrefix = "$this->m_sAttCode{$this->m_sNameSuffix}";
$aRow = array();
$aFieldsMap = array();
if(is_object($linkObjOrId))
if(is_object($linkObjOrId) && (!$linkObjOrId->IsNew()))
{
$key = $linkObjOrId->GetKey();
$iRemoteObjKey = $linkObjOrId->Get($this->m_sExtKeyToRemote);
@@ -130,12 +130,24 @@ class UILinksWidget
else
{
// form for creating a new record
if (is_object($linkObjOrId))
{
// New link existing only in memory
$oNewLinkObj = $linkObjOrId;
$iRemoteObjKey = $oNewLinkObj->Get($this->m_sExtKeyToRemote);
$oRemoteObj = MetaModel::GetObject($this->m_sRemoteClass, $iRemoteObjKey);
$oNewLinkObj->Set($this->m_sExtKeyToMe, $oCurrentObj); // Setting the extkey with the object also fills the related external fields
$linkObjOrId = -$iRemoteObjKey;
}
else
{
$iRemoteObjKey = -$linkObjOrId;
$oNewLinkObj = MetaModel::NewObject($this->m_sLinkedClass);
$oRemoteObj = MetaModel::GetObject($this->m_sRemoteClass, -$linkObjOrId);
$oNewLinkObj->Set($this->m_sExtKeyToRemote, $oRemoteObj); // Setting the extkey with the object alsoo fills the related external fields
$oNewLinkObj->Set($this->m_sExtKeyToMe, $oCurrentObj); // Setting the extkey with the object also fills the related external fields
}
$sPrefix .= "[$linkObjOrId][";
$iRemoteObjKey = -$linkObjOrId;
$oNewLinkObj = MetaModel::NewObject($this->m_sLinkedClass);
$oRemoteObj = MetaModel::GetObject($this->m_sRemoteClass, -$linkObjOrId);
$oNewLinkObj->Set($this->m_sExtKeyToRemote, $oRemoteObj); // Setting the extkey with the object alsoo fills the related external fields
$oNewLinkObj->Set($this->m_sExtKeyToMe, $oCurrentObj); // Setting the extkey with the object also fills the related external fields
$sNameSuffix = "]"; // To make a tabular form
$aArgs['prefix'] = $sPrefix;
$aArgs['wizHelper'] = "oWizardHelper{$this->m_iInputId}_".(-$linkObjOrId);
@@ -275,6 +287,7 @@ EOF
$sHtmlValue = '';
$sTargetClass = self::GetTargetClass($this->m_sClass, $this->m_sAttCode);
$sHtmlValue .= "<div id=\"linkedset_{$this->m_sAttCode}{$this->m_sNameSuffix}\">\n";
$sHtmlValue .= "<input type=\"hidden\" id=\"{$sFormPrefix}{$this->m_iInputId}\">\n";
$oValue->Rewind();
$aForm = array();
while($oCurrentLink = $oValue->Fetch())
@@ -284,7 +297,7 @@ EOF
if ($oCurrentLink->IsNew())
{
$key = -$oLinkedObj->GetKey();
$aForm[$key] = $this->GetFormRow($oPage, $oLinkedObj, $key, $aArgs, $oCurrentObj);
$aForm[$key] = $this->GetFormRow($oPage, $oLinkedObj, $oCurrentLink, $aArgs, $oCurrentObj);
}
else
{
@@ -297,8 +310,9 @@ EOF
$sDuplicates = ($this->m_bDuplicatesAllowed) ? 'true' : 'false';
$sWizHelper = 'oWizardHelper'.$sFormPrefix;
$oPage->add_ready_script(<<<EOF
oWidget{$this->m_iInputId} = new LinksWidget('{$this->m_sAttCode}{$this->m_sNameSuffix}', '{$this->m_sClass}', '{$this->m_sAttCode}', '{$this->m_iInputId}', '{$this->m_sNameSuffix}', $sDuplicates, $sWizHelper);
oWidget{$this->m_iInputId} = new LinksWidget('{$this->m_sAttCode}{$this->m_sNameSuffix}', '{$this->m_sClass}', '{$this->m_sAttCode}', '{$this->m_iInputId}', '{$this->m_sNameSuffix}', $sDuplicates, $sWizHelper, '{$this->m_sExtKeyToRemote}');
oWidget{$this->m_iInputId}.Init();
$('#{$this->m_iInputId}').bind('update_value', function() { $(this).val(oWidget{$this->m_iInputId}.GetUpdatedValue()); })
EOF
);
$sHtmlValue .= "<span style=\"float:left;\">&nbsp;&nbsp;&nbsp;<img src=\"../images/tv-item-last.gif\">&nbsp;&nbsp;<input id=\"{$this->m_sAttCode}{$this->m_sNameSuffix}_btnRemove\" type=\"button\" value=\"".Dict::S('UI:RemoveLinkedObjectsOf_Class')."\" onClick=\"oWidget{$this->m_iInputId}.RemoveSelected();\" >";

View File

@@ -52,8 +52,10 @@ class UIPasswordWidget
{
$sCode = $this->sAttCode.$this->sNameSuffix;
$iWidgetIndex = self::$iWidgetIndex;
$sPasswordValue = utils::ReadPostedParam("attr_{$sCode}[value]", '*****', 'raw_data');
$sConfirmPasswordValue = utils::ReadPostedParam("attr_{$sCode}[confirm]", '*****', 'raw_data');
$aPasswordValues = utils::ReadPostedParam("attr_{$sCode}", null, 'raw_data');
$sPasswordValue = $aPasswordValues ? $aPasswordValues['value'] : '*****';
$sConfirmPasswordValue = $aPasswordValues ? $aPasswordValues['confirm'] : '*****';
$sChangedValue = (($sPasswordValue != '*****') || ($sConfirmPasswordValue != '*****')) ? 1 : 0;
$sHtmlValue = '';
$sHtmlValue = '<input type="password" maxlength="255" name="attr_'.$sCode.'[value]" id="'.$this->iId.'" value="'.htmlentities($sPasswordValue, ENT_QUOTES, 'UTF-8').'"/>&nbsp;<span class="form_validation" id="v_'.$this->iId.'"></span><br/>';

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -508,7 +508,7 @@ class utils
{
// Build an absolute URL to this page on this server/port
$sServerName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '';
$sProtocol = (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS']!="off")) ? 'https' : 'http';
$sProtocol = self::IsConnectionSecure() ? 'https' : 'http';
$iPort = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80;
if ($sProtocol == 'http')
{
@@ -571,6 +571,25 @@ class utils
return $sAppRootUrl;
}
/**
* Helper to handle the variety of HTTP servers
* See #286 (fixed in [896]), and #634 (this fix)
*
* Though the official specs says 'a non empty string', some servers like IIS do set it to 'off' !
* nginx set it to an empty string
* Others might leave it unset (no array entry)
*/
static public function IsConnectionSecure()
{
$bSecured = false;
if (!empty($_SERVER['HTTPS']) && (strtolower($_SERVER['HTTPS']) != 'off'))
{
$bSecured = true;
}
return $bSecured;
}
/**
* Tells whether or not log off operation is supported.
* Actually in only one case:
@@ -747,33 +766,76 @@ class utils
}
/**
* Get the "Back" button to go out of the current environment
* Merge standard menu items with plugin provided menus items
*/
public static function GetEnvironmentBackButton()
public static function GetPopupMenuItems($oPage, $iMenuId, $param, &$aActions, $sTableId = null, $sDataTableId = null)
{
if (isset($_SESSION['itop_return_env']))
// 1st - add standard built-in menu items
//
switch($iMenuId)
{
if (isset($_SESSION['itop_return_url']))
{
$sReturnUrl = $_SESSION['itop_return_url'];
}
else
{
$sReturnUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?switch_env='.$_SESSION['itop_return_env'];
}
return '&nbsp;<button onclick="window;location.href=\''.addslashes($sReturnUrl).'\'">'.Dict::S('UI:Button:Back').'</button>';
case iPopupMenuExtension::MENU_OBJLIST_TOOLKIT:
// $param is a DBObjectSet
$oAppContext = new ApplicationContext();
$sContext = $oAppContext->GetForLink();
$sDataTableId = is_null($sDataTableId) ? '' : $sDataTableId;
$sUIPage = cmdbAbstractObject::ComputeStandardUIPage($param->GetFilter()->GetClass());
$sOQL = addslashes($param->GetFilter()->ToOQL(true));
$sFilter = urlencode($param->GetFilter()->serialize());
$sUrl = utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".$sFilter."&{$sContext}";
$aResult = array(
new SeparatorPopupMenuItem(),
// Static menus: Email this page, CSV Export & Add to Dashboard
new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?body=".urlencode($sUrl).' '), // Add an extra space to make it work in Outlook
new URLPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), $sUrl."&format=csv"),
new JSPopupMenuItem('UI:Menu:AddToDashboard', Dict::S('UI:Menu:AddToDashboard'), "DashletCreationDlg('$sOQL')"),
new JSPopupMenuItem('UI:Menu:ShortcutList', Dict::S('UI:Menu:ShortcutList'), "ShortcutListDlg('$sOQL', '$sDataTableId', '$sContext')"),
);
break;
case iPopupMenuExtension::MENU_OBJDETAILS_ACTIONS:
// $param is a DBObject
$oObj = $param;
$oFilter = DBobjectSearch::FromOQL("SELECT ".get_class($oObj)." WHERE id=".$oObj->GetKey());
$sFilter = $oFilter->serialize();
$sUrl = ApplicationContext::MakeObjectUrl(get_class($oObj), $oObj->GetKey());
$sUIPage = cmdbAbstractObject::ComputeStandardUIPage(get_class($oObj));
$oAppContext = new ApplicationContext();
$sContext = $oAppContext->GetForLink();
$aResult = array(
new SeparatorPopupMenuItem(),
// Static menus: Email this page & CSV Export
new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?subject=".urlencode($oObj->GetRawName())."&body=".urlencode($sUrl).' '), // Add an extra space to make it work in Outlook
new URLPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".urlencode($sFilter)."&format=csv&{$sContext}"),
);
break;
case iPopupMenuExtension::MENU_DASHBOARD_ACTIONS:
// $param is a Dashboard
$oAppContext = new ApplicationContext();
$aParams = $oAppContext->GetAsHash();
$sMenuId = ApplicationMenu::GetActiveNodeId();
$sDlgTitle = addslashes(Dict::S('UI:ImportDashboardTitle'));
$sDlgText = addslashes(Dict::S('UI:ImportDashboardText'));
$sCloseBtn = addslashes(Dict::S('UI:Button:Cancel'));
$aResult = array(
new SeparatorPopupMenuItem(),
new URLPopupMenuItem('UI:ExportDashboard', Dict::S('UI:ExportDashBoard'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=export_dashboard&id='.$sMenuId),
new JSPopupMenuItem('UI:ImportDashboard', Dict::S('UI:ImportDashBoard'), "UploadDashboard({dashboard_id: '$sMenuId', title: '$sDlgTitle', text: '$sDlgText', close_btn: '$sCloseBtn' })"),
);
break;
default:
// Unknown type of menu, do nothing
$aResult = array();
}
else
foreach($aResult as $oMenuItem)
{
return '';
$aActions[$oMenuItem->GetUID()] = $oMenuItem->GetMenuItem();
}
}
/**
* Get the "Back" button to go out of the current environment
*/
public static function GetPopupMenuItems($oPage, $iMenuId, $param, &$aActions)
{
// Invoke the plugins
//
foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance)
{
if (is_object($param) && !($param instanceof DBObject))
@@ -820,6 +882,35 @@ class utils
return $sUrl;
}
/**
* Returns the URL to a page that will execute the requested module page
*
* To be compatible with this mechanism, the called page must include approot
* with an absolute path OR not include it at all (losing the direct access to the page)
* if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__));
* require_once(__DIR__.'/../../approot.inc.php');
*
* @return string ...
*/
static public function GetAbsoluteUrlModulePage($sModule, $sPage, $aArguments = array(), $sEnvironment = null)
{
$sEnvironment = is_null($sEnvironment) ? self::GetCurrentEnvironment() : $sEnvironment;
$aArgs = array();
$aArgs[] = 'exec_module='.$sModule;
$aArgs[] = 'exec_page='.$sPage;
$aArgs[] = 'exec_env='.$sEnvironment;
foreach($aArguments as $sName => $sValue)
{
if (($sName == 'exec_module')||($sName == 'exec_page')||($sName == 'exec_env'))
{
throw new Exception("Module page: $sName is a reserved page argument name");
}
$aArgs[] = $sName.'='.urlencode($sValue);
}
$sArgs = implode('&', $aArgs);
return self::GetAbsoluteUrlAppRoot().'pages/exec.php?'.$sArgs;
}
/**
* Returns a name unique amongst the given list
* @param string $sProposed The default value
@@ -850,6 +941,129 @@ class utils
static public function GetSafeId($sId)
{
return str_replace(array(':', '[', ']', '+', '-'), '_', $sId);
}
}
/**
* Helper to execute an HTTP POST request
* Source: http://netevil.org/blog/2006/nov/http-post-from-php-without-curl
* originaly named after do_post_request
* Does not require cUrl but requires openssl for performing https POSTs.
*
* @param string $sUrl The URL to POST the data to
* @param hash $aData The data to POST as an array('param_name' => value)
* @param string $sOptionnalHeaders Additional HTTP headers as a string with newlines between headers
* @param hash $aResponseHeaders An array to be filled with reponse headers: WARNING: the actual content of the array depends on the library used: cURL or fopen, test with both !! See: http://fr.php.net/manual/en/function.curl-getinfo.php
* @return string The result of the POST request
* @throws Exception
*/
static public function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null)
{
// $sOptionnalHeaders is a string containing additional HTTP headers that you would like to send in your request.
if (function_exists('curl_init'))
{
// If cURL is available, let's use it, since it provides a greater control over the various HTTP/SSL options
// For instance fopen does not allow to work around the bug: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112
// by setting the SSLVERSION to 3 as done below.
$aHeaders = explode("\n", $sOptionnalHeaders);
$aHTTPHeaders = array();
foreach($aHeaders as $sHeaderString)
{
if(preg_match('/^([^:]): (.+)$/', $sHeaderString, $aMatches))
{
$aHTTPHeaders[$aMatches[1]] = $aMatches[2];
}
}
$aOptions = array(
CURLOPT_RETURNTRANSFER => true, // return the content of the request
CURLOPT_HEADER => false, // don't return the headers in the output
CURLOPT_FOLLOWLOCATION => true, // follow redirects
CURLOPT_ENCODING => "", // handle all encodings
CURLOPT_USERAGENT => "spider", // who am i
CURLOPT_AUTOREFERER => true, // set referer on redirect
CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect
CURLOPT_TIMEOUT => 120, // timeout on response
CURLOPT_MAXREDIRS => 10, // stop after 10 redirects
CURLOPT_SSL_VERIFYPEER => false, // Disabled SSL Cert checks
CURLOPT_SSLVERSION => 3, // MUST to prevent a strange SSL error: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112
CURLOPT_POST => count($aData),
CURLOPT_POSTFIELDS => http_build_query($aData),
CURLOPT_HTTPHEADER => $aHTTPHeaders,
);
$ch = curl_init($sUrl);
curl_setopt_array($ch, $aOptions);
$response = curl_exec($ch);
$iErr = curl_errno($ch);
$sErrMsg = curl_error( $ch );
$aHeaders = curl_getinfo( $ch );
if ($iErr !== 0)
{
throw new Exception("Problem opening URL: $sUrl, $sErrMsg");
}
if (is_array($aResponseHeaders))
{
$aHeaders = curl_getinfo($ch);
foreach($aHeaders as $sCode => $sValue)
{
$sName = str_replace(' ' , '-', ucwords(str_replace('_', ' ', $sCode))); // Transform "content_type" into "Content-Type"
$aResponseHeaders[$sName] = $sValue;
}
}
curl_close( $ch );
}
else
{
// cURL is not available let's try with streams and fopen...
$sData = http_build_query($aData);
$aParams = array('http' => array(
'method' => 'POST',
'content' => $sData,
'header'=> "Content-type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($sData)."\r\n",
));
if ($sOptionnalHeaders !== null)
{
$aParams['http']['header'] .= $sOptionnalHeaders;
}
$ctx = stream_context_create($aParams);
$fp = @fopen($sUrl, 'rb', false, $ctx);
if (!$fp)
{
global $php_errormsg;
if (isset($php_errormsg))
{
throw new Exception("Wrong URL: $sUrl, $php_errormsg");
}
elseif ((strtolower(substr($sUrl, 0, 5)) == 'https') && !extension_loaded('openssl'))
{
throw new Exception("Cannot connect to $sUrl: missing module 'openssl'");
}
else
{
throw new Exception("Wrong URL: $sUrl");
}
}
$response = @stream_get_contents($fp);
if ($response === false)
{
throw new Exception("Problem reading data from $sUrl, $php_errormsg");
}
if (is_array($aResponseHeaders))
{
$aMeta = stream_get_meta_data($fp);
$aHeaders = $aMeta['wrapper_data'];
foreach($aHeaders as $sHeaderString)
{
if(preg_match('/^([^:]+): (.+)$/', $sHeaderString, $aMatches))
{
$aResponseHeaders[$aMatches[1]] = trim($aMatches[2]);
}
}
}
}
return $response;
}
}
?>
?>

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -492,18 +492,25 @@ class WebPage implements Page
{
MetaModel::RecordQueryTrace();
}
if (class_exists('ExecutionKPI'))
{
ExecutionKPI::ReportStats();
}
}
/**
* Build a series of hidden field[s] from an array
*/
// By Rom - je verrais bien une serie d'outils pour gerer des parametres que l'on retransmet entre pages d'un wizard...
// ptet deriver webpage en webwizard
public function add_input_hidden($sLabel, $aData)
{
foreach($aData as $sKey=>$sValue)
foreach($aData as $sKey => $sValue)
{
$this->add("<input type=\"hidden\" name=\"".$sLabel."[$sKey]\" value=\"$sValue\">");
// Note: protection added to protect against the Notice 'array to string conversion' that appeared with PHP 5.4
// (this function seems unused though!)
if (is_scalar($sValue))
{
$this->add("<input type=\"hidden\" name=\"".$sLabel."[$sKey]\" value=\"$sValue\">");
}
}
}
@@ -650,7 +657,8 @@ class WebPage implements Page
foreach ($aActions as $aAction)
{
$sClass = isset($aAction['class']) ? " class=\"{$aAction['class']}\"" : "";
$sOnClick = isset($aAction['onclick']) ? " onclick=\"{$aAction['onclick']}\"" : "";
$sOnClick = isset($aAction['onclick']) ? ' onclick="'.htmlspecialchars($aAction['onclick'], ENT_QUOTES, "UTF-8").'"' : '';
$sTarget = isset($aAction['target']) ? " target=\"{$aAction['target']}\"" : "";
if (empty($aAction['url']))
{
if ($sPrevUrl != '') // Don't output consecutively two separators...
@@ -661,42 +669,340 @@ class WebPage implements Page
}
else
{
$sHtml .= "<li><a href=\"{$aAction['url']}\"$sClass $sOnClick>{$aAction['label']}</a></li>";
$sHtml .= "<li><a $sTarget href=\"{$aAction['url']}\"$sClass $sOnClick>{$aAction['label']}</a></li>";
$sPrevUrl = $aAction['url'];
}
}
$sHtml .= "</ul></li></ul></div>";
foreach(array_reverse($aFavoriteActions) as $aAction)
{
$sHtml .= "<div class=\"actions_button\"><a href='{$aAction['url']}'>{$aAction['label']}</a></div>";
$sTarget = isset($aAction['target']) ? " target=\"{$aAction['target']}\"" : "";
$sHtml .= "<div class=\"actions_button\"><a $sTarget href='{$aAction['url']}'>{$aAction['label']}</a></div>";
}
return $sHtml;
}
protected function output_dict_entries()
protected function output_dict_entries($bReturnOutput = false)
{
$sHtml = '';
if (count($this->a_dict_entries)>0)
{
echo "<script type=\"text/javascript\">\n";
echo "var Dict = {};\n";
echo "Dict._entries = {};\n";
echo "Dict.S = function(sEntry) {\n";
echo " if (sEntry in Dict._entries)\n";
echo " {\n";
echo " return Dict._entries[sEntry];\n";
echo " }\n";
echo " else\n";
echo " {\n";
echo " return sEntry;\n";
echo " }\n";
echo "};\n";
$sHtml .= "<script type=\"text/javascript\">\n";
$sHtml .= "var Dict = {};\n";
$sHtml .= "Dict._entries = {};\n";
$sHtml .= "Dict.S = function(sEntry) {\n";
$sHtml .= " if (sEntry in Dict._entries)\n";
$sHtml .= " {\n";
$sHtml .= " return Dict._entries[sEntry];\n";
$sHtml .= " }\n";
$sHtml .= " else\n";
$sHtml .= " {\n";
$sHtml .= " return sEntry;\n";
$sHtml .= " }\n";
$sHtml .= "};\n";
foreach($this->a_dict_entries as $s_entry => $s_value)
{
echo "Dict._entries['$s_entry'] = '".addslashes($s_value)."';\n";
$sHtml .= "Dict._entries['$s_entry'] = '".addslashes($s_value)."';\n";
}
echo "</script>\n";
$sHtml .= "</script>\n";
}
if ($bReturnOutput)
{
return $sHtml;
}
else
{
echo $sHtml;
}
}
}
?>
interface iTabbedPage
{
public function AddTabContainer($sTabContainer, $sPrefix = '');
public function AddToTab($sTabContainer, $sTabLabel, $sHtml);
public function SetCurrentTabContainer($sTabContainer = '');
public function SetCurrentTab($sTabLabel = '');
/**
* Add a tab which content will be loaded asynchronously via the supplied URL
*
* Limitations:
* Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to pull content from another server.
* Static content cannot be added inside such tabs.
*
* @param string $sTabLabel The (localised) label of the tab
* @param string $sUrl The URL to load (on the same server)
* @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause the tab to be reloaded upon each activation.
* @since 2.0.3
*/
public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true);
public function GetCurrentTab();
public function RemoveTab($sTabLabel, $sTabContainer = null);
/**
* Finds the tab whose title matches a given pattern
* @return mixed The name of the tab as a string or false if not found
*/
public function FindTab($sPattern, $sTabContainer = null);
}
/**
* Helper class to implement JQueryUI tabs inside a page
*/
class TabManager
{
protected $m_aTabs;
protected $m_sCurrentTabContainer;
protected $m_sCurrentTab;
public function __construct()
{
$this->m_aTabs = array();
$this->m_sCurrentTabContainer = '';
$this->m_sCurrentTab = '';
}
public function AddTabContainer($sTabContainer, $sPrefix = '')
{
$this->m_aTabs[$sTabContainer] = array('prefix' => $sPrefix, 'tabs' => array());
return "\$Tabs:$sTabContainer\$";
}
public function AddToCurrentTab($sHtml)
{
$this->AddToTab($this->m_sCurrentTabContainer, $this->m_sCurrentTab, $sHtml);
}
public function GetCurrentTabLength($sHtml)
{
$iLength = isset($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html']) ? strlen($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html']): 0;
return $iLength;
}
/**
* Truncates the given tab to the specifed length and returns the truncated part
* @param string $sTabContainer The tab container in which to truncate the tab
* @param string $sTab The name/identifier of the tab to truncate
* @param integer $iLength The length/offset at which to truncate the tab
* @return string The truncated part
*/
public function TruncateTab($sTabContainer, $sTab, $iLength)
{
$sResult = substr($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'], $iLength);
$this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'] = substr($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'], 0, $iLength);
return $sResult;
}
public function TabExists($sTabContainer, $sTab)
{
return isset($this->m_aTabs[$sTabContainer]['tabs'][$sTab]);
}
public function TabsContainerCount()
{
return count($this->m_aTabs);
}
public function AddToTab($sTabContainer, $sTabLabel, $sHtml)
{
if (!isset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]))
{
// Set the content of the tab
$this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel] = array(
'type' => 'html',
'html' => $sHtml,
);
}
else
{
if ($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['type'] != 'html')
{
throw new Exception("Cannot add HTML content to the tab '$sTabLabel' of type '{$this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['type']}'");
}
// Append to the content of the tab
$this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['html'] .= $sHtml;
}
return ''; // Nothing to add to the page for now
}
public function SetCurrentTabContainer($sTabContainer = '')
{
$sPreviousTabContainer = $this->m_sCurrentTabContainer;
$this->m_sCurrentTabContainer = $sTabContainer;
return $sPreviousTabContainer;
}
public function SetCurrentTab($sTabLabel = '')
{
$sPreviousTab = $this->m_sCurrentTab;
$this->m_sCurrentTab = $sTabLabel;
return $sPreviousTab;
}
/**
* Add a tab which content will be loaded asynchronously via the supplied URL
*
* Limitations:
* Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to pull content from another server.
* Static content cannot be added inside such tabs.
*
* @param string $sTabLabel The (localised) label of the tab
* @param string $sUrl The URL to load (on the same server)
* @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause the tab to be reloaded upon each activation.
* @since 2.0.3
*/
public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true)
{
// Set the content of the tab
$this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$sTabLabel] = array(
'type' => 'ajax',
'url' => $sUrl,
'cache' => $bCache,
);
return ''; // Nothing to add to the page for now
}
public function GetCurrentTabContainer()
{
return $this->m_sCurrentTabContainer;
}
public function GetCurrentTab()
{
return $this->m_sCurrentTab;
}
public function RemoveTab($sTabLabel, $sTabContainer = null)
{
if ($sTabContainer == null)
{
$sTabContainer = $this->m_sCurrentTabContainer;
}
if (isset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]))
{
// Delete the content of the tab
unset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]);
// If we just removed the active tab, let's reset the active tab
if (($this->m_sCurrentTabContainer == $sTabContainer) && ($this->m_sCurrentTab == $sTabLabel))
{
$this->m_sCurrentTab = '';
}
}
}
/**
* Finds the tab whose title matches a given pattern
* @return mixed The name of the tab as a string or false if not found
*/
public function FindTab($sPattern, $sTabContainer = null)
{
$return = false;
if ($sTabContainer == null)
{
$sTabContainer = $this->m_sCurrentTabContainer;
}
foreach($this->m_aTabs[$sTabContainer]['tabs'] as $sTabLabel => $void)
{
if (preg_match($sPattern, $sTabLabel))
{
$result = $sTabLabel;
break;
}
}
return $result;
}
/**
* Make the given tab the active one, as if it were clicked
* DOES NOT WORK: apparently in the *old* version of jquery
* that we are using this is not supported... TO DO upgrade
* the whole jquery bundle...
*/
public function SelectTab($sTabContainer, $sTabLabel)
{
$container_index = 0;
$tab_index = 0;
foreach($this->m_aTabs as $sCurrentTabContainerName => $aTabs)
{
if ($sTabContainer == $sCurrentTabContainerName)
{
foreach($aTabs['tabs'] as $sCurrentTabLabel => $void)
{
if ($sCurrentTabLabel == $sTabLabel)
{
break;
}
$tab_index++;
}
break;
}
$container_index++;
}
$sSelector = '#tabbedContent_'.$container_index.' > ul';
return "window.setTimeout(\"$('$sSelector').tabs('select', $tab_index);\", 100);"; // Let the time to the tabs widget to initialize
}
public function RenderIntoContent($sContent)
{
// Render the tabs in the page (if any)
foreach($this->m_aTabs as $sTabContainerName => $aTabs)
{
$sTabs = '';
$sPrefix = $aTabs['prefix'];
$container_index = 0;
if (count($aTabs['tabs']) > 0)
{
$sTabs = "<!-- tabs -->\n<div id=\"tabbedContent_{$sPrefix}{$container_index}\" class=\"light\">\n";
$sTabs .= "<ul>\n";
// Display the unordered list that will be rendered as the tabs
$i = 0;
foreach($aTabs['tabs'] as $sTabName => $aTabData)
{
switch($aTabData['type'])
{
case 'ajax':
$sTabs .= "<li data-cache=\"".($aTabData['cache'] ? 'true' : 'false')."\"><a href=\"{$aTabData['url']}\" class=\"tab\"><span>".htmlentities($sTabName, ENT_QUOTES, 'UTF-8')."</span></a></li>\n";
break;
case 'html':
default:
$sTabs .= "<li><a href=\"#tab_{$sPrefix}{$container_index}$i\" class=\"tab\"><span>".htmlentities($sTabName, ENT_QUOTES, 'UTF-8')."</span></a></li>\n";
}
$i++;
}
$sTabs .= "</ul>\n";
// Now add the content of the tabs themselves
$i = 0;
foreach($aTabs['tabs'] as $sTabName => $aTabData)
{
switch($aTabData['type'])
{
case 'ajax':
// Nothing to add
break;
case 'html':
default:
$sTabs .= "<div id=\"tab_{$sPrefix}{$container_index}$i\">".$aTabData['html']."</div>\n";
}
$i++;
}
$sTabs .= "</div>\n<!-- end of tabs-->\n";
}
$sContent = str_replace("\$Tabs:$sTabContainerName\$", $sTabs, $sContent);
$container_index++;
}
return $sContent;
}
}

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -37,69 +37,77 @@ class XMLPage extends WebPage
var $m_bPassThrough;
var $m_bHeaderSent;
function __construct($s_title, $bPassThrough = false)
{
parent::__construct($s_title);
$this->m_bPassThrough = $bPassThrough;
$this->m_bHeaderSent = false;
$this->add_header("Content-type: text/xml; charset=utf-8");
function __construct($s_title, $bPassThrough = false)
{
parent::__construct($s_title);
$this->m_bPassThrough = $bPassThrough;
$this->m_bHeaderSent = false;
$this->add_header("Content-type: text/xml; charset=utf-8");
$this->add_header("Cache-control: no-cache");
$this->add_header("Content-location: export.xml");
}
}
public function output()
{
if (!$this->m_bPassThrough)
{
$this->add("<?xml version=\"1.0\" encoding=\"UTF-8\"?".">\n");
$this->add_header("Content-Length: ".strlen(trim($this->s_content)));
foreach($this->a_headers as $s_header)
{
header($s_header);
}
echo trim($this->s_content);
}
if (class_exists('MetaModel'))
{
MetaModel::RecordQueryTrace();
}
}
public function add($sText)
{
if (!$this->m_bPassThrough)
{
parent::add($sText);
}
else
{
if ($this->m_bHeaderSent)
{
echo $sText;
}
else
{
$s_captured_output = ob_get_contents();
ob_end_clean();
foreach($this->a_headers as $s_header)
{
header($s_header);
}
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?".">\n";
echo trim($s_captured_output);
echo trim($this->s_content);
echo $sText;
$this->m_bHeaderSent = true;
}
}
}
public function small_p($sText)
{
public function output()
{
if (!$this->m_bPassThrough)
{
$this->s_content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?".">\n".trim($this->s_content);
$this->add_header("Content-Length: ".strlen($this->s_content));
foreach($this->a_headers as $s_header)
{
header($s_header);
}
echo $this->s_content;
}
if (class_exists('MetaModel'))
{
MetaModel::RecordQueryTrace();
}
}
public function add($sText)
{
if (!$this->m_bPassThrough)
{
parent::add($sText);
}
else
{
if ($this->m_bHeaderSent)
{
echo $sText;
}
else
{
$s_captured_output = ob_get_contents();
ob_end_clean();
foreach($this->a_headers as $s_header)
{
header($s_header);
}
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?".">\n";
echo trim($s_captured_output);
echo trim($this->s_content);
echo $sText;
$this->m_bHeaderSent = true;
}
}
}
public function small_p($sText)
{
}
public function table($aConfig, $aData, $aParams = array())
{
}
public function TrashUnexpectedOutput()
{
if (!$this->m_bPassThrough)
{
parent::TrashUnexpectedOutput();
}
}
}
?>

View File

@@ -219,7 +219,7 @@ class MyHelpers
}
}
public static function get_callstack_html($iLevelsToIgnore = 0, $aCallStack = null)
public static function get_callstack($iLevelsToIgnore = 0, $aCallStack = null)
{
if ($aCallStack == null) $aCallStack = debug_backtrace();
@@ -231,6 +231,16 @@ class MyHelpers
{
$sLine = empty($aCallInfo['line']) ? "" : $aCallInfo['line'];
$sFile = empty($aCallInfo['file']) ? "" : $aCallInfo['file'];
if ($sFile != '')
{
$sFile = str_replace('\\', '/', $sFile);
$sAppRoot = str_replace('\\', '/', APPROOT);
$iPos = strpos($sFile, $sAppRoot);
if ($iPos !== false)
{
$sFile = substr($sFile, strlen($sAppRoot));
}
}
$sClass = empty($aCallInfo['class']) ? "" : $aCallInfo['class'];
$sType = empty($aCallInfo['type']) ? "" : $aCallInfo['type'];
$sFunction = empty($aCallInfo['function']) ? "" : $aCallInfo['function'];
@@ -259,11 +269,11 @@ class MyHelpers
$args .= $a;
break;
case 'string':
$a = Str::pure2html(self::beautifulstr($a, 1024, true, true));
$a = Str::pure2html(self::beautifulstr($a, 64, true, false));
$args .= "\"$a\"";
break;
case 'array':
$args .= 'Array('.count($a).')';
$args .= 'array('.count($a).')';
break;
case 'object':
$args .= 'Object('.get_class($a).')';
@@ -272,19 +282,25 @@ class MyHelpers
$args .= 'Resource('.strstr($a, '#').')';
break;
case 'boolean':
$args .= $a ? 'True' : 'False';
$args .= $a ? 'true' : 'false';
break;
case 'NULL':
$args .= 'Null';
$args .= 'null';
break;
default:
$args .= 'Unknown';
}
}
$sFunctionInfo = "$sClass $sType $sFunction($args)";
$sFunctionInfo = "$sClass$sType$sFunction($args)";
}
$aDigestCallStack[] = array('File'=>$sFile, 'Line'=>$sLine, 'Function'=>$sFunctionInfo);
}
return $aDigestCallStack;
}
public static function get_callstack_html($iLevelsToIgnore = 0, $aCallStack = null)
{
$aDigestCallStack = self::get_callstack($iLevelsToIgnore, $aCallStack);
return self::make_table_from_assoc_array($aDigestCallStack);
}
@@ -293,6 +309,17 @@ class MyHelpers
return self::get_callstack_html($iLevelsToIgnore, $aCallStack);
}
public static function get_callstack_text($iLevelsToIgnore = 0, $aCallStack = null)
{
$aDigestCallStack = self::get_callstack($iLevelsToIgnore, $aCallStack);
$aRes = array();
foreach ($aDigestCallStack as $aCall)
{
$aRes[] = $aCall['File'].' at '.$aCall['Line'].', '.$aCall['Function'];
}
return implode("\n", $aRes);
}
///////////////////////////////////////////////////////////////////////////////
// Source: New
// Last modif: 2004/12/20 RQU

View File

@@ -213,8 +213,12 @@ class ActionEmail extends ActionNotification
$aRecipients = array();
while ($oObj = $oSet->Fetch())
{
$aRecipients[] = $oObj->Get($sEmailAttCode);
$this->m_iRecipients++;
$sAddress = trim($oObj->Get($sEmailAttCode));
if (strlen($sAddress) > 0)
{
$aRecipients[] = $sAddress;
$this->m_iRecipients++;
}
}
return implode(', ', $aRecipients);
}

View File

@@ -34,27 +34,27 @@ class ExecAsyncTask implements iBackgroundProcess
public function Process($iTimeLimit)
{
$sOQL = "SELECT AsyncTask WHERE ISNULL(started) AND (ISNULL(planned) OR (planned < NOW()))";
$oSet = new CMDBObjectSet(DBObjectSearch::FromOQL($sOQL), array('created' => true) /* order by*/, array());
$sNow = date('Y-m-d H:i:s');
// Criteria: planned, and expected to occur... ASAP or in the past
$sOQL = "SELECT AsyncTask WHERE (status = 'planned') AND (ISNULL(planned) OR (planned < '$sNow'))";
$iProcessed = 0;
while ((time() < $iTimeLimit) && ($oTask = $oSet->Fetch()))
while (time() < $iTimeLimit)
{
$oTask->Set('started', time());
$oTask->DBUpdate();
$oTask->Process();
// Next one ?
$oSet = new CMDBObjectSet(DBObjectSearch::FromOQL($sOQL), array('created' => true) /* order by*/, array(), null, 1 /* limit count */);
$oTask = $oSet->Fetch();
if (is_null($oTask))
{
// Nothing to be done
break;
}
$iProcessed++;
$oTask->DBDelete();
}
if ($iProcessed == $oSet->Count())
{
return "processed $iProcessed tasks";
}
else
{
return "processed $iProcessed tasks (remaining: ".($oSet->Count() - $iProcessed).")";
if ($oTask->Process())
{
$oTask->DBDelete();
}
}
return "processed $iProcessed tasks";
}
}
@@ -80,36 +80,141 @@ abstract class AsyncTask extends DBObject
"display_template" => "",
);
MetaModel::Init_Params($aParams);
//MetaModel::Init_InheritAttributes();
// MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
// Null is allowed to ease the migration from iTop 2.0.2 and earlier, when the status did not exist, and because the default value is not taken into account in the SQL definition
// The value is set from null to planned in the setup program
MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('planned,running,idle,error'), "sql"=>"status", "default_value"=>"planned", "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeDateTime("created", array("allowed_values"=>null, "sql"=>"created", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeDateTime("started", array("allowed_values"=>null, "sql"=>"started", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
// planned... still not used - reserved for timer management
MetaModel::Init_AddAttribute(new AttributeDateTime("planned", array("allowed_values"=>null, "sql"=>"planned", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeExternalKey("event_id", array("targetclass"=>"Event", "jointype"=> "", "allowed_values"=>null, "sql"=>"event_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_SILENT, "depends_on"=>array())));
// Display lists
// MetaModel::Init_SetZListItems('details', array()); // Attributes to be displayed for the complete details
// MetaModel::Init_SetZListItems('list', array()); // Attributes to be displayed for a list
// Search criteria
// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
MetaModel::Init_AddAttribute(new AttributeInteger("remaining_retries", array("allowed_values"=>null, "sql"=>"remaining_retries", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array())));
}
/**
* Every is fine
*/
const OK = 0;
/**
* The task no longer exists
*/
const DELETED = 1;
/**
* The task is already being executed
*/
const ALREADY_RUNNING = 2;
/**
* The current process requests the ownership on the task.
* In case the task can be accessed concurrently, this function can be overloaded to add a critical section.
* The function must not block the caller if another process is already owning the task
*
* @return integer A code among OK/DELETED/ALREADY_RUNNING.
*/
public function MarkAsRunning()
{
try
{
$this->Set('status', 'running');
$this->Set('started', time());
$this->DBUpdate();
return self::OK;
}
catch(Exception $e)
{
// Corrupted task !! (for example: "Failed to reload object")
IssueLog::Error('Failed to process async task #'.$this->GetKey().' - reason: '.$e->getMessage().' - fatal error, deleting the task.');
if ($this->Get('event_id') != 0)
{
$oEventLog = MetaModel::GetObject('Event', $this->Get('event_id'));
$oEventLog->Set('message', 'Failed, corrupted data: '.$e->getMessage());
$oEventLog->DBUpdate();
}
$this->DBDelete();
return self::DELETED;
}
}
public function GetRetryDelay()
{
$iRetryDelay = 600;
$aRetries = MetaModel::GetConfig()->Get('async_task_retries', array());
if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries))
{
$aConfig = $aRetries[get_class($this)];
$iRetryDelay = $aConfig['retry_delay'];
}
return $iRetryDelay;
}
public function GetMaxRetries()
{
$iMaxRetries = 0;
$aRetries = MetaModel::GetConfig()->Get('async_task_retries', array());
if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries))
{
$aConfig = $aRetries[get_class($this)];
$iMaxRetries = $aConfig['max_retries'];
}
}
protected function OnInsert()
{
$this->Set('created', time());
$this->Set('remaining_retries', $this->GetMaxRetries());
}
public function Process()
/**
* @return boolean True if the task record can be deleted
*/
public function Process()
{
$sStatus = $this->DoProcess();
if ($this->Get('event_id') != 0)
{
$oEventLog = MetaModel::GetObject('Event', $this->Get('event_id'));
$oEventLog->Set('message', $sStatus);
$oEventLog->DBUpdate();
// By default: consider that the task is not completed
$bRet = false;
// Attempt to take the ownership
$iStatus = $this->MarkAsRunning();
if ($iStatus == self::OK)
{
try
{
$sStatus = $this->DoProcess();
if ($this->Get('event_id') != 0)
{
$oEventLog = MetaModel::GetObject('Event', $this->Get('event_id'));
$oEventLog->Set('message', $sStatus);
$oEventLog->DBUpdate();
}
$bRet = true;
}
catch(Exception $e)
{
$iRemaining = $this->Get('remaining_retries');
if ($iRemaining > 0)
{
$iRetryDelay = $this->GetRetryDelay();
IssueLog::Info('Failed to process async task #'.$this->GetKey().' - reason: '.$e->getMessage().' - remaining retries: '.$iRemaining.' - next retry in '.$iRetryDelay.'s');
$this->Set('remaining_retries', $iRemaining - 1);
$this->Set('status', 'planned');
$this->Set('started', null);
$this->Set('planned', time() + $iRetryDelay);
$this->DBUpdate();
}
else
{
IssueLog::Error('Failed to process async task #'.$this->GetKey().' - reason: '.$e->getMessage());
}
}
}
else
{
// Already done or being handled by another process... skip...
$bRet = false;
}
return $bRet;
}
abstract public function DoProcess();
@@ -139,7 +244,8 @@ class AsyncSendEmail extends AsyncTask
MetaModel::Init_Params($aParams);
MetaModel::Init_InheritAttributes();
MetaModel::Init_AddAttribute(new AttributeText("to", array("allowed_values"=>null, "sql"=>"to", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeInteger("version", array("allowed_values"=>null, "sql"=>"version", "default_value"=>Email::ORIGINAL_FORMAT, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeText("to", array("allowed_values"=>null, "sql"=>"to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeText("subject", array("allowed_values"=>null, "sql"=>"subject", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeLongText("message", array("allowed_values"=>null, "sql"=>"message", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
@@ -161,7 +267,10 @@ class AsyncSendEmail extends AsyncTask
$oNew->Set('to', $oEMail->GetRecipientTO(true /* string */));
$oNew->Set('subject', $oEMail->GetSubject());
$sMessage = serialize($oEMail);
// $oNew->Set('version', 1);
// $sMessage = serialize($oEMail);
$oNew->Set('version', 2);
$sMessage = $oEMail->SerializeV2();
$oNew->Set('message', $sMessage);
$oNew->DBInsert();
}
@@ -169,7 +278,20 @@ class AsyncSendEmail extends AsyncTask
public function DoProcess()
{
$sMessage = $this->Get('message');
$oEMail = unserialize($sMessage);
$iVersion = (int) $this->Get('version');
switch($iVersion)
{
case Email::FORMAT_V2:
$oEMail = Email::UnSerializeV2($sMessage);
break;
case Email::ORIGINAL_FORMAT:
$oEMail = unserialize($sMessage);
break;
default:
return 'Unknown version of the serialization format: '.$iVersion;
}
$iRes = $oEMail->Send($aIssues, true /* force synchro !!!!! */);
switch ($iRes)
{

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -81,6 +81,8 @@ define('DEL_MOVEUP', 3);
*
* @package iTopORM
*/
define('ATTRIBUTE_TRACKING_NONE', 0); // Do not track changes of the attribute
define('ATTRIBUTE_TRACKING_ALL', 3); // Do track all changes of the attribute
define('LINKSET_TRACKING_NONE', 0); // Do not track changes in the link set
define('LINKSET_TRACKING_LIST', 1); // Do track added/removed items
define('LINKSET_TRACKING_DETAILS', 2); // Do track modified items
@@ -90,6 +92,7 @@ define('LINKSET_EDITMODE_NONE', 0); // The linkset cannot be edited at all from
define('LINKSET_EDITMODE_ADDONLY', 1); // The only possible action is to open a new window to create a new object
define('LINKSET_EDITMODE_ACTIONS', 2); // Show the usual 'Actions' popup menu
define('LINKSET_EDITMODE_INPLACE', 3); // The "linked" objects can be created/modified/deleted in place
define('LINKSET_EDITMODE_ADDREMOVE', 4); // The "linked" objects can be added/removed in place
/**
@@ -208,19 +211,17 @@ abstract class AttributeDefinition
public function IsExternalField() {return false;}
public function IsWritable() {return false;}
public function LoadInObject() {return true;}
public function AlwaysLoadInTables() {return $this->GetOptional('always_load_in_tables', false);}
public function GetValue($oHostObject){return null;} // must return the value if LoadInObject returns false
public function IsNullAllowed() {return true;}
public function GetCode() {return $this->m_sCode;}
public function GetLabel($sDefault = null)
/**
* Helper to browse the hierarchy of classes, searching for a label
*/
protected function SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly)
{
// If no default value is specified, let's define the most relevant one for developping purposes
if (is_null($sDefault))
{
$sDefault = str_replace('_', ' ', $this->m_sCode);
}
$sLabel = Dict::S('Class:'.$this->m_sHostClass.'/Attribute:'.$this->m_sCode, '');
$sLabel = Dict::S('Class:'.$this->m_sHostClass.$sDictEntrySuffix, '', $bUserLanguageOnly);
if (strlen($sLabel) == 0)
{
// Nothing found: go higher in the hierarchy (if possible)
@@ -232,13 +233,29 @@ abstract class AttributeDefinition
if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode))
{
$oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode);
$sLabel = $oAttDef->GetLabel($sDefault);
$sLabel = $oAttDef->SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly);
}
}
}
return $sLabel;
}
public function GetLabel($sDefault = null)
{
$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, null, true /*user lang*/);
if (is_null($sLabel))
{
// If no default value is specified, let's define the most relevant one for developping purposes
if (is_null($sDefault))
{
$sDefault = str_replace('_', ' ', $this->m_sCode);
}
// Browse the hierarchy again, accepting default (english) translations
$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, $sDefault, false);
}
return $sLabel;
}
/**
* Get the label corresponding to the given value (in plain text)
* To be overloaded for localized enums
@@ -272,52 +289,32 @@ abstract class AttributeDefinition
public function GetDescription($sDefault = null)
{
// If no default value is specified, let's define the most relevant one for developping purposes
if (is_null($sDefault))
$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', null, true /*user lang*/);
if (is_null($sLabel))
{
$sDefault = '';
}
$sLabel = Dict::S('Class:'.$this->m_sHostClass.'/Attribute:'.$this->m_sCode.'+', '');
if (strlen($sLabel) == 0)
{
// Nothing found: go higher in the hierarchy (if possible)
//
$sLabel = $sDefault;
$sParentClass = MetaModel::GetParentClass($this->m_sHostClass);
if ($sParentClass)
// If no default value is specified, let's define the most relevant one for developping purposes
if (is_null($sDefault))
{
if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode))
{
$oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode);
$sLabel = $oAttDef->GetDescription($sDefault);
}
$sDefault = '';
}
// Browse the hierarchy again, accepting default (english) translations
$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', $sDefault, false);
}
return $sLabel;
}
}
public function GetHelpOnEdition($sDefault = null)
{
// If no default value is specified, let's define the most relevant one for developping purposes
if (is_null($sDefault))
$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', null, true /*user lang*/);
if (is_null($sLabel))
{
$sDefault = '';
}
$sLabel = Dict::S('Class:'.$this->m_sHostClass.'/Attribute:'.$this->m_sCode.'?', '');
if (strlen($sLabel) == 0)
{
// Nothing found: go higher in the hierarchy (if possible)
//
$sLabel = $sDefault;
$sParentClass = MetaModel::GetParentClass($this->m_sHostClass);
if ($sParentClass)
// If no default value is specified, let's define the most relevant one for developping purposes
if (is_null($sDefault))
{
if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode))
{
$oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode);
$sLabel = $oAttDef->GetHelpOnEdition($sDefault);
}
$sDefault = '';
}
// Browse the hierarchy again, accepting default (english) translations
$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', $sDefault, false);
}
return $sLabel;
}
@@ -348,6 +345,12 @@ abstract class AttributeDefinition
return $this->GetDescription();
}
}
public function GetTrackingLevel()
{
return $this->GetOptional('tracking_level', ATTRIBUTE_TRACKING_ALL);
}
public function GetValuesDef() {return null;}
public function GetPrerequisiteAttributes() {return array();}
@@ -433,6 +436,26 @@ abstract class AttributeDefinition
return (string)$sValue;
}
/**
* Helper to get a value that will be JSON encoded
* The operation is the opposite to FromJSONToValue
*/
public function GetForJSON($value)
{
// In most of the cases, that will be the expected behavior...
return $this->GetEditValue($value);
}
/**
* Helper to form a value, given JSON decoded data
* The operation is the opposite to GetForJSON
*/
public function FromJSONToValue($json)
{
// Passthrough in most of the cases
return $json;
}
/**
* Override to display the value in the GUI
*/
@@ -471,11 +494,14 @@ abstract class AttributeDefinition
$sLabel = $this->GetLabel();
}
$sNewValueHtml = $this->GetAsHTML($sNewValue);
$sOldValueHtml = $this->GetAsHTML($sOldValue);
if($this->IsExternalKey())
{
$sTargetClass = $this->GetTargetClass();
$sOldValue = (int)$sOldValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sOldValue) : null;
$sNewValue = (int)$sNewValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sNewValue) : null;
$sOldValueHtml = (int)$sOldValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sOldValue) : null;
$sNewValueHtml = (int)$sNewValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sNewValue) : null;
}
if ( (($this->GetType() == 'String') || ($this->GetType() == 'Text')) &&
(strlen($sNewValue) > strlen($sOldValue)) )
@@ -483,27 +509,27 @@ abstract class AttributeDefinition
// Check if some text was not appended to the field
if (substr($sNewValue,0, strlen($sOldValue)) == $sOldValue) // Text added at the end
{
$sDelta = substr($sNewValue, strlen($sOldValue));
$sDelta = $this->GetAsHTML(substr($sNewValue, strlen($sOldValue)));
$sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel);
}
else if (substr($sNewValue, -strlen($sOldValue)) == $sOldValue) // Text added at the beginning
{
$sDelta = substr($sNewValue, 0, strlen($sNewValue) - strlen($sOldValue));
$sDelta = $this->GetAsHTML(substr($sNewValue, 0, strlen($sNewValue) - strlen($sOldValue)));
$sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel);
}
else
{
if (strlen($sOldValue) == 0)
{
$sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValue);
$sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml);
}
else
{
if (is_null($sNewValue))
{
$sNewValue = Dict::S('UI:UndefinedObject');
$sNewValueHtml = Dict::S('UI:UndefinedObject');
}
$sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, $sNewValue, $sOldValue);
$sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, $sNewValueHtml, $sOldValueHtml);
}
}
}
@@ -511,15 +537,15 @@ abstract class AttributeDefinition
{
if (strlen($sOldValue) == 0)
{
$sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValue);
$sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml);
}
else
{
if (is_null($sNewValue))
{
$sNewValue = Dict::S('UI:UndefinedObject');
$sNewValueHtml = Dict::S('UI:UndefinedObject');
}
$sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, $sNewValue, $sOldValue);
$sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, $sNewValueHtml, $sOldValueHtml);
}
}
return $sResult;
@@ -595,7 +621,7 @@ class AttributeLinkedSet extends AttributeDefinition
public function GetTrackingLevel()
{
return $this->GetOptional('tracking_level', LINKSET_TRACKING_LIST);
return $this->GetOptional('tracking_level', MetaModel::GetConfig()->Get('tracking_level_linked_set_default'));
}
public function GetEditMode()
@@ -893,7 +919,7 @@ class AttributeLinkedSetIndirect extends AttributeLinkedSet
public function GetTrackingLevel()
{
return $this->GetOptional('tracking_level', LINKSET_TRACKING_ALL);
return $this->GetOptional('tracking_level', MetaModel::GetConfig()->Get('tracking_level_linked_set_indirect_default'));
}
}
@@ -1214,14 +1240,14 @@ class AttributeDecimal extends AttributeDBField
public function MakeRealValue($proposedValue, $oHostObj)
{
if (is_null($proposedValue)) return null;
if ($proposedValue == '') return null;
if ($proposedValue === '') return null;
return (string)$proposedValue;
}
public function ScalarToSQL($value)
{
assert(is_null($value) || preg_match('/'.$this->GetValidationPattern().'/', $value));
return (string)$value; // treated as a string
return $value; // null or string
}
}
@@ -1267,6 +1293,14 @@ class AttributeBoolean extends AttributeInteger
{
return $sValue ? '1' : '0';
}
/**
* Helper to get a value that will be JSON encoded
* The operation is the opposite to FromJSONToValue
*/
public function GetForJSON($value)
{
return $value ? '1' : '0';
}
}
/**
@@ -1677,12 +1711,6 @@ class AttributeEncryptedString extends AttributeString
// Example: [[Server:db1.tnut.com]]
define('WIKI_OBJECT_REGEXP', '/\[\[(.+):(.+)\]\]/U');
// <url>
// Example: http://romain:trustno1@127.0.0.1:8888/iTop-trunk/modules/itop-caches/itop-caches.php?agument=machin%20#monAncre
define('WIKI_URL', "/(https?|ftp)\:\/\/([a-z0-9+!*(),;?&=\$_.-]+(\:[a-z0-9+!*(),;?&=\$_.-]+)?@)?([a-z0-9-.]{3,})(\:[0-9]{2,5})?(\/([a-z0-9+\$_-]\.?)+)*\/?(\?[a-z+&\$_.-][a-z0-9;:@&%=+\/\$_.-]*)?(#[a-z_.-][a-z0-9+\$_.-]*)?/i");
// SHEME............. USER.................... PASSWORD...................... HOST/IP......... PORT.......... PATH...................... GET................................... ANCHOR....................
// Origin of this regexp: http://www.php.net/manual/fr/function.preg-match.php#93824
/**
* Map a text column (size > ?) to an attribute
@@ -1703,7 +1731,8 @@ class AttributeText extends AttributeString
static public function RenderWikiHtml($sText)
{
if (preg_match_all(WIKI_URL, $sText, $aAllMatches, PREG_SET_ORDER /* important !*/ |PREG_OFFSET_CAPTURE /* important ! */))
$sPattern = '/'.str_replace('/', '\/', utils::GetConfig()->Get('url_validation_pattern')).'/i';
if (preg_match_all($sPattern, $sText, $aAllMatches, PREG_SET_ORDER /* important !*/ |PREG_OFFSET_CAPTURE /* important ! */))
{
$aUrls = array();
$i = count($aAllMatches);
@@ -1894,7 +1923,12 @@ class AttributeCaseLog extends AttributeLongText
// Facilitate things: allow the user to Set the value from a string
public function MakeRealValue($proposedValue, $oHostObj)
{
if (!($proposedValue instanceof ormCaseLog))
if ($proposedValue instanceof ormCaseLog)
{
// Passthrough
$ret = $proposedValue;
}
else
{
// Append the new value if an instance of the object is supplied
//
@@ -1916,13 +1950,21 @@ class AttributeCaseLog extends AttributeLongText
{
$oCaseLog = new ormCaseLog();
}
if (strlen($proposedValue) > 0)
if ($proposedValue instanceof stdClass)
{
$oCaseLog->AddLogEntry(parent::MakeRealValue($proposedValue, $oHostObj));
$oCaseLog->AddLogEntryFromJSON($proposedValue);
}
return $oCaseLog;
else
{
if (strlen($proposedValue) > 0)
{
$oCaseLog->AddLogEntry(parent::MakeRealValue($proposedValue, $oHostObj));
}
}
$ret = $oCaseLog;
}
return $proposedValue;
return $ret;
}
public function GetSQLExpressions($sPrefix = '')
@@ -2040,6 +2082,45 @@ class AttributeCaseLog extends AttributeLongText
return '';
}
}
/**
* Helper to get a value that will be JSON encoded
* The operation is the opposite to FromJSONToValue
*/
public function GetForJSON($value)
{
return $value->GetForJSON();
}
/**
* Helper to form a value, given JSON decoded data
* The operation is the opposite to GetForJSON
*/
public function FromJSONToValue($json)
{
if (is_string($json))
{
// Will be correctly handled in MakeRealValue
$ret = $json;
}
else
{
if (isset($json->add_item))
{
// Will be correctly handled in MakeRealValue
$ret = $json->add_item;
if (!isset($ret->message))
{
throw new Exception("Missing mandatory entry: 'message'");
}
}
else
{
$ret = ormCaseLog::FromJSON($json);
}
}
return $ret;
}
}
/**
@@ -2066,8 +2147,7 @@ class AttributeEmailAddress extends AttributeString
{
public function GetValidationPattern()
{
// return "^([0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\\w]*[0-9a-zA-Z]\\.)+[a-zA-Z]{2,9})$";
return "^[a-zA-Z0-9._&-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$";
return $this->GetOptional('validation_pattern', '^'.utils::GetConfig()->Get('email_validation_pattern').'$');
}
public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
@@ -2224,19 +2304,12 @@ class AttributeEnum extends AttributeString
}
else
{
$sLabel = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue, '');
if (strlen($sLabel) == 0)
$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, null, true /*user lang*/);
if (is_null($sLabel))
{
$sLabel = str_replace('_', ' ', $sValue);
$sParentClass = MetaModel::GetParentClass($this->m_sHostClass);
if ($sParentClass)
{
if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode))
{
$oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode);
$sLabel = $oAttDef->GetValueLabel($sValue);
}
}
$sDefault = str_replace('_', ' ', $sValue);
// Browse the hierarchy again, accepting default (english) translations
$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, $sDefault, false);
}
}
return $sLabel;
@@ -2251,7 +2324,7 @@ class AttributeEnum extends AttributeString
}
else
{
$sDescription = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue.'+', '');
$sDescription = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue.'+', '', true /* user language only */);
if (strlen($sDescription) == 0)
{
$sParentClass = MetaModel::GetParentClass($this->m_sHostClass);
@@ -2333,12 +2406,13 @@ class AttributeEnum extends AttributeString
}
}
public function GetAsHTMLForHistory($sOldValue, $sNewValue, $sLabel = null)
/**
* Helper to get a value that will be JSON encoded
* The operation is the opposite to FromJSONToValue
*/
public function GetForJSON($value)
{
$sOldValue = is_null($sOldValue) ? null : $this->GetAsHTML($sOldValue);
$sNewValue = is_null($sNewValue) ? null : $this->GetAsHTML($sNewValue);
$sResult = parent::GetAsHTMLForHistory($sOldValue, $sNewValue, $sLabel);
return $sResult;
return $value;
}
public function GetAllowedValues($aArgs = array(), $sContains = '')
@@ -2564,7 +2638,7 @@ class AttributeDateTime extends AttributeDBField
$sFrom = array("\r\n", $sTextQualifier);
$sTo = array("\n", $sTextQualifier.$sTextQualifier);
$sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
return '"'.$sEscaped.'"';
return $sTextQualifier.$sEscaped.$sTextQualifier;
}
/**
@@ -2648,10 +2722,6 @@ class AttributeDuration extends AttributeInteger
protected function GetSQLCol() {return "INT(11) UNSIGNED";}
public function GetNullValue() {return '0';}
public function GetDefaultValue()
{
return 0;
}
public function MakeRealValue($proposedValue, $oHostObj)
{
@@ -2841,7 +2911,15 @@ class AttributeExternalKey extends AttributeDBFieldVoid
public function GetDefaultValue() {return 0;}
public function IsNullAllowed() {return $this->Get("is_null_allowed");}
public function IsNullAllowed()
{
if (MetaModel::GetConfig()->Get('disable_mandatory_ext_keys'))
{
return true;
}
return $this->Get("is_null_allowed");
}
public function GetBasicFilterOperators()
{
@@ -3175,6 +3253,12 @@ class AttributeExternalField extends AttributeDefinition
throw new CoreException("Unexpected value for argument iType: '$iType'");
}
}
public function GetPrerequisiteAttributes()
{
return array($this->Get("extkey_attcode"));
}
public function GetExtAttDef()
{
@@ -3302,17 +3386,17 @@ class AttributeURL extends AttributeString
$sTarget = $this->Get("target");
if (empty($sTarget)) $sTarget = "_blank";
$sLabel = Str::pure2html($sValue);
if (strlen($sLabel) > 40)
if (strlen($sLabel) > 255)
{
// Truncate the length to about 40 characters, by removing the middle
$sLabel = substr($sLabel, 0, 25).'...'.substr($sLabel, -15);
// Truncate the length to 128 characters, by removing the middle
$sLabel = substr($sLabel, 0, 100).'.....'.substr($sLabel, -20);
}
return "<a target=\"$sTarget\" href=\"$sValue\">$sLabel</a>";
}
public function GetValidationPattern()
{
return "^(http|https|ftp)\://[a-zA-Z0-9\-\.]+(:[a-zA-Z0-9]*)?/?([a-zA-Z0-9\-\._\?\,\'/\\\+&amp;%\$#\=~])*$";
return $this->GetOptional('validation_pattern', '^'.utils::GetConfig()->Get('url_validation_pattern').'$');
}
}
@@ -3336,6 +3420,11 @@ class AttributeBlob extends AttributeDefinition
public function GetDefaultValue() {return "";}
public function IsNullAllowed() {return $this->GetOptional("is_null_allowed", false);}
public function GetEditValue($sValue, $oHostObj = null)
{
return '';
}
// Facilitate things: allow the user to Set the value from a string
public function MakeRealValue($proposedValue, $oHostObj)
@@ -3363,26 +3452,26 @@ class AttributeBlob extends AttributeDefinition
public function FromSQLToValue($aCols, $sPrefix = '')
{
if (!isset($aCols[$sPrefix]))
if (!array_key_exists($sPrefix, $aCols))
{
$sAvailable = implode(', ', array_keys($aCols));
throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}");
}
$sMimeType = $aCols[$sPrefix];
$sMimeType = isset($aCols[$sPrefix]) ? $aCols[$sPrefix] : '';
if (!isset($aCols[$sPrefix.'_data']))
if (!array_key_exists($sPrefix.'_data', $aCols))
{
$sAvailable = implode(', ', array_keys($aCols));
throw new MissingColumnException("Missing column '".$sPrefix."_data' from {$sAvailable}");
}
$data = $aCols[$sPrefix.'_data'];
$data = isset($aCols[$sPrefix.'_data']) ? $aCols[$sPrefix.'_data'] : null;
if (!isset($aCols[$sPrefix.'_filename']))
if (!array_key_exists($sPrefix.'_filename', $aCols))
{
$sAvailable = implode(', ', array_keys($aCols));
throw new MissingColumnException("Missing column '".$sPrefix."_filename' from {$sAvailable}");
}
$sFileName = $aCols[$sPrefix.'_filename'];
$sFileName = isset($aCols[$sPrefix.'_filename']) ? $aCols[$sPrefix.'_filename'] : '';
$value = new ormDocument($data, $sMimeType, $sFileName);
return $value;
@@ -3464,6 +3553,44 @@ class AttributeBlob extends AttributeDefinition
{
return ''; // Not exportable in XML, or as CDATA + some subtags ??
}
/**
* Helper to get a value that will be JSON encoded
* The operation is the opposite to FromJSONToValue
*/
public function GetForJSON($value)
{
if ($value instanceOf ormDocument)
{
$aValues = array();
$aValues['data'] = base64_encode($value->GetData());
$aValues['mimetype'] = $value->GetMimeType();
$aValues['filename'] = $value->GetFileName();
}
else
{
$aValues = null;
}
return $aValues;
}
/**
* Helper to form a value, given JSON decoded data
* The operation is the opposite to GetForJSON
*/
public function FromJSONToValue($json)
{
if (isset($json->data))
{
$data = base64_decode($json->data);
$value = new ormDocument($data, $json->mimetype, $json->filename);
}
else
{
$value = null;
}
return $value;
}
}
/**
@@ -3790,6 +3917,12 @@ class AttributeStopWatch extends AttributeDefinition
throw new CoreException("Unknown item code '$sItemCode' for attribute ".$this->GetHostClass().'::'.$this->GetCode());
}
protected function GetBooleanLabel($bValue)
{
$sDictKey = $bValue ? 'yes' : 'no';
return Dict::S('BooleanLabel:'.$sDictKey, 'def:'.$sDictKey);
}
public function GetSubItemAsHTMLForHistory($sItemCode, $sOldValue, $sNewValue, $sLabel)
{
switch($sItemCode)
@@ -3820,12 +3953,12 @@ class AttributeStopWatch extends AttributeDefinition
$sHtmlNew = (int)$sNewValue ? date(self::GetDateFormat(true /*full*/), (int)$sNewValue) : null;
break;
case 'passed':
$sHtmlOld = (int)$sOldValue ? '1' : '0';
$sHtmlNew = (int)$sNewValue ? '1' : '0';
$sHtmlOld = $this->GetBooleanLabel((int)$sOldValue);
$sHtmlNew = $this->GetBooleanLabel((int)$sNewValue);
break;
case 'triggered':
$sHtmlOld = (int)$sOldValue ? '1' : '0';
$sHtmlNew = (int)$sNewValue ? '1' : '0';
$sHtmlOld = $this->GetBooleanLabel((int)$sOldValue);
$sHtmlNew = $this->GetBooleanLabel((int)$sNewValue);
break;
case 'overrun':
$sHtmlOld = (int)$sOldValue > 0 ? AttributeDuration::FormatDuration((int)$sOldValue) : '';
@@ -3894,10 +4027,8 @@ class AttributeStopWatch extends AttributeDefinition
}
break;
case 'passed':
$sHtml = $value ? '1' : '0';
break;
case 'triggered':
$sHtml = $value ? '1' : '0';
$sHtml = $this->GetBooleanLabel($value);
break;
case 'overrun':
$sHtml = Str::pure2html(AttributeDuration::FormatDuration($value));
@@ -3911,12 +4042,141 @@ class AttributeStopWatch extends AttributeDefinition
public function GetSubItemAsCSV($sItemCode, $value, $sSeparator = ',', $sTextQualifier = '"')
{
return $value;
$sFrom = array("\r\n", $sTextQualifier);
$sTo = array("\n", $sTextQualifier.$sTextQualifier);
$sEscaped = str_replace($sFrom, $sTo, (string)$value);
$sRet = $sTextQualifier.$sEscaped.$sTextQualifier;
switch($sItemCode)
{
case 'timespent':
case 'started':
case 'laststart':
case 'stopped':
break;
default:
foreach ($this->ListThresholds() as $iThreshold => $aFoo)
{
$sThPrefix = $iThreshold.'_';
if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
{
// The current threshold is concerned
$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
switch($sThresholdCode)
{
case 'deadline':
break;
case 'passed':
case 'triggered':
$sRet = $sTextQualifier.$this->GetBooleanLabel($value).$sTextQualifier;
break;
case 'overrun':
break;
}
}
}
}
return $sRet;
}
public function GetSubItemAsXML($sItemCode, $value)
{
return Str::pure2xml((string)$value);
$sRet = Str::pure2xml((string)$value);
switch($sItemCode)
{
case 'timespent':
case 'started':
case 'laststart':
case 'stopped':
break;
default:
foreach ($this->ListThresholds() as $iThreshold => $aFoo)
{
$sThPrefix = $iThreshold.'_';
if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
{
// The current threshold is concerned
$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
switch($sThresholdCode)
{
case 'deadline':
break;
case 'passed':
case 'triggered':
$sRet = $this->GetBooleanLabel($value);
break;
case 'overrun':
break;
}
}
}
}
return $sRet;
}
/**
* Implemented for the HTML spreadsheet format!
*/
public function GetSubItemAsEditValue($sItemCode, $value)
{
$sRet = $value;
switch($sItemCode)
{
case 'timespent':
break;
case 'started':
case 'laststart':
case 'stopped':
if (is_null($value))
{
$sRet = ''; // Undefined
}
else
{
$sRet = date(self::GetDateFormat(), $value);
}
break;
default:
foreach ($this->ListThresholds() as $iThreshold => $aFoo)
{
$sThPrefix = $iThreshold.'_';
if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
{
// The current threshold is concerned
$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
switch($sThresholdCode)
{
case 'deadline':
if ($value)
{
$sRet = date(self::GetDateFormat(true /*full*/), $value);
}
else
{
$sRet = '';
}
break;
case 'passed':
case 'triggered':
$sRet = $this->GetBooleanLabel($value);
break;
case 'overrun':
break;
}
}
}
}
return $sRet;
}
}
@@ -4026,7 +4286,7 @@ class AttributeSubItem extends AttributeDefinition
public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true)
{
$oParent = $this->GetTargetAttDef();
$res = $oParent->GetSubItemAsCSV($this->Get('item_code'), $value, $sSeparator = ',', $sTextQualifier = '"');
$res = $oParent->GetSubItemAsCSV($this->Get('item_code'), $value, $sSeparator, $sTextQualifier);
return $res;
}
@@ -4045,6 +4305,16 @@ class AttributeSubItem extends AttributeDefinition
$sValue = $oParent->GetSubItemAsHTMLForHistory($this->Get('item_code'), $sOldValue, $sNewValue, $sLabel);
return $sValue;
}
/**
* As of now, this function must be implemented to have the value in spreadsheet format
*/
public function GetEditValue($value, $oHostObj = null)
{
$oParent = $this->GetTargetAttDef();
$res = $oParent->GetSubItemAsEditValue($this->Get('item_code'), $value);
return $res;
}
}
/**
@@ -4092,19 +4362,19 @@ class AttributeOneWayPassword extends AttributeDefinition
public function FromSQLToValue($aCols, $sPrefix = '')
{
if (!isset($aCols[$sPrefix]))
if (!array_key_exists($sPrefix, $aCols))
{
$sAvailable = implode(', ', array_keys($aCols));
throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}");
}
$hashed = $aCols[$sPrefix];
$hashed = isset($aCols[$sPrefix]) ? $aCols[$sPrefix] : '';
if (!isset($aCols[$sPrefix.'_salt']))
if (!array_key_exists($sPrefix.'_salt', $aCols))
{
$sAvailable = implode(', ', array_keys($aCols));
throw new MissingColumnException("Missing column '".$sPrefix."_salt' from {$sAvailable}");
}
$sSalt = $aCols[$sPrefix.'_salt'];
$sSalt = isset($aCols[$sPrefix.'_salt']) ? $aCols[$sPrefix.'_salt'] : '';
$value = new ormPassword($hashed, $sSalt);
return $value;
@@ -4429,7 +4699,7 @@ class AttributeComputedFieldVoid extends AttributeDefinition
public function GetEditClass() {return "";}
public function GetValuesDef() {return null;}
public function GetPrerequisiteAttributes() {return $this->Get("depends_on");}
public function GetPrerequisiteAttributes() {return $this->GetOptional("depends_on", array());}
public function IsDirectField() {return true;}
public function IsScalar() {return true;}
@@ -4471,7 +4741,7 @@ class AttributeComputedFieldVoid extends AttributeDefinition
public function GetBasicFilterOperators()
{
return array();
return array("="=>"equals", "!="=>"differs from");
}
public function GetBasicFilterLooseOperator()
{

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -18,17 +18,50 @@
/**
* Class BackgroundProcess
* interface iProcess
* Something that can be executed
*
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
interface iProcess
{
public function Process($iUnixTimeLimit);
}
/**
* interface iBackgroundProcess
* Any extension that must be called regularly to be executed in the background
*
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
interface iBackgroundProcess
interface iBackgroundProcess extends iProcess
{
/*
Gives the repetition rate in seconds
@returns integer
*/
public function GetPeriodicity();
public function Process($iUnixTimeLimit);
}
/**
* interface iScheduledProcess
* A variant of process that must be called at specific times
*
* @copyright Copyright (C) 2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
interface iScheduledProcess extends iProcess
{
/*
Gives the exact time at which the process must be run next time
@returns DateTime
*/
public function GetNextOccurrence();
}
?>

View File

@@ -0,0 +1,76 @@
<?php
// Copyright (C) 2013 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/>
/**
* Class BackgroundTask
* A class to record information about the execution of background processes
*
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
class BackgroundTask extends DBObject
{
public static function Init()
{
$aParams = array
(
"category" => "core/cmdb",
"key_type" => "autoincrement",
"name_attcode" => "class_name",
"state_attcode" => "",
"reconc_keys" => array(),
"db_table" => "priv_backgroundtask",
"db_key_field" => "id",
"db_finalclass_field" => "",
"display_template" => "",
);
MetaModel::Init_Params($aParams);
MetaModel::Init_AddAttribute(new AttributeString("class_name", array("allowed_values"=>null, "sql"=>"class_name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeDateTime("first_run_date", array("allowed_values"=>null, "sql"=>"first_run_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeDateTime("latest_run_date", array("allowed_values"=>null, "sql"=>"latest_run_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeDateTime("next_run_date", array("allowed_values"=>null, "sql"=>"next_run_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeInteger("total_exec_count", array("allowed_values"=>null, "sql"=>"total_exec_count", "default_value"=>"0", "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeDecimal("latest_run_duration", array("allowed_values"=>null, "sql"=>"latest_run_duration", "digits"=> 8, "decimals"=> 3, "default_value"=>"0", "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeDecimal("min_run_duration", array("allowed_values"=>null, "sql"=>"min_run_duration", "digits"=> 8, "decimals"=> 3, "default_value"=>"0", "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeDecimal("max_run_duration", array("allowed_values"=>null, "sql"=>"max_run_duration", "digits"=> 8, "decimals"=> 3, "default_value"=>"0", "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeDecimal("average_run_duration", array("allowed_values"=>null, "sql"=>"average_run_duration", "digits"=> 8, "decimals"=> 3, "default_value"=>"0", "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeBoolean("running", array("allowed_values"=>null, "sql"=>"running", "default_value"=>false, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('active,paused'), "sql"=>"status", "default_value"=>'active', "is_null_allowed"=>false, "depends_on"=>array())));
}
public function ComputeDurations($fLatestDuration)
{
$iTotalRun = $this->Get('total_exec_count');
$fAverageDuration = ($this->Get('average_run_duration') * $iTotalRun + $fLatestDuration) / (1+$iTotalRun);
$this->Set('average_run_duration', sprintf('%.3f',$fAverageDuration));
$this->Set('total_exec_count', 1+$iTotalRun);
if ($fLatestDuration < $this->Get('min_run_duration'))
{
$this->Set('min_run_duration', sprintf('%.3f',$fLatestDuration));
}
if ($fLatestDuration > $this->Get('max_run_duration'))
{
$this->Set('max_run_duration', sprintf('%.3f',$fLatestDuration));
}
$this->Set('latest_run_duration', sprintf('%.3f',$fLatestDuration));
}
}

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -260,6 +260,7 @@ class BulkChange
protected $m_aOnDisappear; // array of attcode => value, values to be set when an object gets out of scope (ignored if no scope has been defined)
protected $m_sDateFormat; // Date format specification, see utils::StringToTime()
protected $m_bLocalizedValues; // Values in the data set are localized (see AttributeEnum)
protected $m_aExtKeysMappingCache; // Cache for resolving external keys based on the given search criterias
public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null, $bLocalize = false)
{
@@ -272,6 +273,7 @@ class BulkChange
$this->m_aOnDisappear = $aOnDisappear;
$this->m_sDateFormat = $sDateFormat;
$this->m_bLocalizedValues = $bLocalize;
$this->m_aExtKeysMappingCache = array();
}
protected $m_bReportHtml = false;
@@ -296,8 +298,17 @@ class BulkChange
$oReconFilter = new CMDBSearchFilter($oExtKey->GetTargetClass());
foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol)
{
// The foreign attribute is one of our reconciliation key
$oReconFilter->AddCondition($sForeignAttCode, $aRowData[$iCol], '=');
if ($sForeignAttCode == 'id')
{
$value = (int) $aRowData[$iCol];
}
else
{
// The foreign attribute is one of our reconciliation key
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode);
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
}
$oReconFilter->AddCondition($sForeignAttCode, $value, '=');
$aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
}
@@ -356,27 +367,73 @@ class BulkChange
else
{
$oReconFilter = new CMDBSearchFilter($oExtKey->GetTargetClass());
$aCacheKeys = array();
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
{
// The foreign attribute is one of our reconciliation key
$oReconFilter->AddCondition($sForeignAttCode, $aRowData[$iCol], '=');
if ($sForeignAttCode == 'id')
{
$value = $aRowData[$iCol];
}
else
{
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode);
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
}
$aCacheKeys[] = $value;
$oReconFilter->AddCondition($sForeignAttCode, $value, '=');
$aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
}
$oExtObjects = new CMDBObjectSet($oReconFilter);
switch($oExtObjects->Count())
$sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query...
$iCount = 0;
$iForeignKey = null;
$sOQL = '';
// TODO: check if *too long* keys can lead to collisions... and skip the cache in such a case...
if (!array_key_exists($sAttCode, $this->m_aExtKeysMappingCache))
{
case 0:
$this->m_aExtKeysMappingCache[$sAttCode] = array();
}
if (array_key_exists($sCacheKey, $this->m_aExtKeysMappingCache[$sAttCode]))
{
// Cache hit
$iCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c'];
$iForeignKey = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['k'];
$sOQL = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['oql'];
// Record the hit
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['h']++;
}
else
{
// Cache miss, let's initialize it
$oExtObjects = new CMDBObjectSet($oReconFilter);
$iCount = $oExtObjects->Count();
if ($iCount == 1)
{
$oForeignObj = $oExtObjects->Fetch();
$iForeignKey = $oForeignObj->GetKey();
}
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array(
'c' => $iCount,
'k' => $iForeignKey,
'oql' => $oReconFilter->ToOql(),
'h' => 0, // number of hits on this cache entry
);
}
switch($iCount)
{
case 0:
$aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound');
$aResults[$sAttCode]= new CellStatus_SearchIssue();
break;
case 1:
case 1:
// Do change the external key attribute
$oForeignObj = $oExtObjects->Fetch();
$oTargetObj->Set($sAttCode, $oForeignObj->GetKey());
$oTargetObj->Set($sAttCode, $iForeignKey);
break;
default:
default:
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $oExtObjects->Count());
$aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $oExtObjects->Count(), $oReconFilter->ToOql());
$aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iCount, $sOQL);
}
}
@@ -770,8 +827,11 @@ class BulkChange
{
$aVisited = array();
}
$iPreviousTimeLimit = ini_get('max_execution_time');
$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
foreach($this->m_aData as $iRow => $aRowData)
{
set_time_limit($iLoopTimeLimit);
if (isset($aResult[$iRow]["__STATUS__"]))
{
// An issue at the earlier steps - skip the rest
@@ -824,7 +884,15 @@ class BulkChange
{
// The value is given in the data row
$iCol = $this->m_aAttList[$sAttCode];
$valuecondition = $aRowData[$iCol];
if ($sAttCode == 'id')
{
$valuecondition = $aRowData[$iCol];
}
else
{
$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
$valuecondition = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
}
}
if (is_null($valuecondition))
{
@@ -882,11 +950,13 @@ class BulkChange
$iObj = $oObj->GetKey();
if (!in_array($iObj, $aVisited))
{
set_time_limit($iLoopTimeLimit);
$iRow++;
$this->UpdateMissingObject($aResult, $iRow, $oObj, $oChange);
}
}
}
set_time_limit($iPreviousTimeLimit);
// Fill in the blanks - the result matrix is expected to be 100% complete
//
@@ -930,17 +1000,17 @@ class BulkChange
$oPage->add('<div id="'.$sAjaxDivId.'">');
}
$oPage->p(Dict::S('UI:History:BulkImports+'));
$oPage->p(Dict::S('UI:History:BulkImports+').' <span id="csv_history_reload"></span>');
$oBulkChangeSearch = DBObjectSearch::FromOQL("SELECT CMDBChange WHERE userinfo LIKE '%(CSV)'");
$oBulkChangeSearch = DBObjectSearch::FromOQL("SELECT CMDBChange WHERE origin IN ('csv-interactive', 'csv-import.php')");
$iQueryLimit = $bShowAll ? 0 : MetaModel::GetConfig()->GetMaxDisplayLimit() + 1;
$iQueryLimit = $bShowAll ? 0 : appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit());
$oBulkChanges = new DBObjectSet($oBulkChangeSearch, array('date' => false), array(), null, $iQueryLimit);
$oAppContext = new ApplicationContext();
$bLimitExceeded = false;
if ($oBulkChanges->Count() > MetaModel::GetConfig()->GetMaxDisplayLimit())
if ($oBulkChanges->Count() > (appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit())))
{
$bLimitExceeded = true;
if (!$bShowAll)
@@ -998,7 +1068,6 @@ class BulkChange
{
$aDetails[] = array('date' => $sDate, 'user' => $sUser, 'class' => $sClass, 'created' => $iCreated, 'modified' => $iModified);
}
}
$aConfig = array( 'date' => array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')),
@@ -1018,7 +1087,7 @@ class BulkChange
else
{
// Truncated list
$iMinDisplayLimit = MetaModel::GetConfig()->GetMinDisplayLimit();
$iMinDisplayLimit = appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit());
$sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oBulkChanges->Count());
$sLinkLabel = Dict::S('UI:DisplayAll');
$oPage->add('<p>'.$sCollapsedLabel.'&nbsp;&nbsp;<a class="truncated" onclick="OnTruncatedHistoryToggle(true);">'.$sLinkLabel.'</p>');
@@ -1036,6 +1105,7 @@ EOF
<<<EOF
function OnTruncatedHistoryToggle(bShowAll)
{
$('#csv_history_reload').html('<img src="../images/indicator.gif"/>');
$.get(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?{$sAppContext}', {operation: 'displayCSVHistory', showall: bShowAll}, function(data)
{
$('#$sAjaxDivId').html(data);

View File

@@ -44,11 +44,15 @@ class CMDBChange extends DBObject
"db_table" => "priv_change",
"db_key_field" => "id",
"db_finalclass_field" => "",
'indexes' => array(
array('origin'),
)
);
MetaModel::Init_Params($aParams);
//MetaModel::Init_InheritAttributes();
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 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())));
}
// Helper to keep track of the author of a given change,

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -45,6 +45,9 @@ class CMDBChangeOp extends DBObject
"db_table" => "priv_changeop",
"db_key_field" => "id",
"db_finalclass_field" => "optype",
'indexes' => array(
array('objclass', 'objkey'),
)
);
MetaModel::Init_Params($aParams);
//MetaModel::Init_InheritAttributes();

View File

@@ -93,7 +93,8 @@ abstract class CMDBObject extends DBObject
// Note: this value is static, but that could be changed because it is sometimes a real issue (see update of interfaces / connected_to
protected static $m_oCurrChange = null;
protected static $m_sInfo = null; // null => the information is built in a standard way
protected static $m_sOrigin = null; // null => the origin is 'interactive'
/**
* Specify another change (this is mainly for backward compatibility)
*/
@@ -134,6 +135,15 @@ abstract class CMDBObject extends DBObject
self::$m_sInfo = $sInfo;
}
/**
* Provides information about the origin of the change
* @param $sOrigin String: one of: interactive, csv-interactive, csv-import.php, webservice-soap, webservice-rest, syncho-data-source, email-processing, custom-extension
*/
public static function SetTrackOrigin($sOrigin)
{
self::$m_sOrigin = $sOrigin;
}
/**
* Get the additional information (defaulting to user name)
*/
@@ -148,7 +158,22 @@ abstract class CMDBObject extends DBObject
return self::$m_sInfo;
}
}
/**
* Get the 'origin' information (defaulting to 'interactive')
*/
protected static function GetTrackOrigin()
{
if (is_null(self::$m_sOrigin))
{
return 'interactive';
}
else
{
return self::$m_sOrigin;
}
}
/**
* Create a standard change record (done here 99% of the time, and nearly once per page)
*/
@@ -157,11 +182,27 @@ abstract class CMDBObject extends DBObject
self::$m_oCurrChange = MetaModel::NewObject("CMDBChange");
self::$m_oCurrChange->Set("date", time());
self::$m_oCurrChange->Set("userinfo", self::GetTrackInfo());
self::$m_oCurrChange->Set("origin", self::GetTrackOrigin());
self::$m_oCurrChange->DBInsert();
}
protected function RecordObjCreation()
{
// Delete any existing change tracking about the current object (IDs can be reused due to InnoDb bug; see TRAC #886)
//
// 1 - remove the deletion record(s)
// Note that objclass contain the ROOT class
$oFilter = new DBObjectSearch('CMDBChangeOpDelete');
$oFilter->AddCondition('objclass', MetaModel::GetRootClass(get_class($this)), '=');
$oFilter->AddCondition('objkey', $this->GetKey(), '=');
MetaModel::PurgeData($oFilter);
// 2 - any other change tracking information left prior to 2.0.3 (when the purge of the history has been implemented in RecordObjDeletion
// In that case, objclass is the final class of the object
$oFilter = new DBObjectSearch('CMDBChangeOp');
$oFilter->AddCondition('objclass', get_class($this), '=');
$oFilter->AddCondition('objkey', $this->GetKey(), '=');
MetaModel::PurgeData($oFilter);
parent::RecordObjCreation();
$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpCreate");
$oMyChangeOp->Set("objclass", get_class($this));
@@ -171,12 +212,20 @@ abstract class CMDBObject extends DBObject
protected function RecordObjDeletion($objkey)
{
$sRootClass = MetaModel::GetRootClass(get_class($this));
// Delete any existing change tracking about the current object
$oFilter = new DBObjectSearch('CMDBChangeOp');
$oFilter->AddCondition('objclass', get_class($this), '=');
$oFilter->AddCondition('objkey', $objkey, '=');
MetaModel::PurgeData($oFilter);
parent::RecordObjDeletion($objkey);
$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpDelete");
$oMyChangeOp->Set("objclass", MetaModel::GetRootClass(get_class($this)));
$oMyChangeOp->Set("objkey", $objkey);
$oMyChangeOp->Set("fclass", get_class($this));
$oMyChangeOp->Set("fname", $this->GetRawName());
$oMyChangeOp->Set("fname", substr($this->GetRawName(), 0, 255)); // Protect against very long friendly names
$iId = $oMyChangeOp->DBInsertNoReload();
}
@@ -189,8 +238,9 @@ abstract class CMDBObject extends DBObject
foreach ($aValues as $sAttCode=> $value)
{
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
if ($oAttDef->IsExternalField()) continue; // #@# temporary
if ($oAttDef->IsLinkSet()) continue; // #@# temporary
if ($oAttDef->IsExternalField()) continue;
if ($oAttDef->IsLinkSet()) continue;
if ($oAttDef->GetTrackingLevel() == ATTRIBUTE_TRACKING_NONE) continue;
if (array_key_exists($sAttCode, $aOrigValues))
{
@@ -322,6 +372,18 @@ abstract class CMDBObject extends DBObject
$oMyChangeOp->Set("newvalue", $value ? 1 : 0);
$iId = $oMyChangeOp->DBInsertNoReload();
}
elseif ($oAttDef instanceOf AttributeHierarchicalKey)
{
// Hierarchical keys
//
$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
$oMyChangeOp->Set("objclass", get_class($this));
$oMyChangeOp->Set("objkey", $this->GetKey());
$oMyChangeOp->Set("attcode", $sAttCode);
$oMyChangeOp->Set("oldvalue", $original);
$oMyChangeOp->Set("newvalue", $value[$sAttCode]);
$iId = $oMyChangeOp->DBInsertNoReload();
}
else
{
// Scalars

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2014 Combodo SARL
//
// This file is part of iTop.
//
@@ -29,10 +29,18 @@ require_once(APPROOT.'core/kpi.class.inc.php');
class MySQLException extends CoreException
{
public function __construct($sIssue, $aContext)
public function __construct($sIssue, $aContext, $oException = null)
{
$aContext['mysql_error'] = CMDBSource::GetError();
$aContext['mysql_errno'] = CMDBSource::GetErrNo();;
if ($oException != null)
{
$aContext['mysql_error'] = $oException->getCode();
$aContext['mysql_errno'] = $oException->getMessage();
}
else
{
$aContext['mysql_error'] = CMDBSource::GetError();
$aContext['mysql_errno'] = CMDBSource::GetErrNo();
}
parent::__construct($sIssue, $aContext);
}
}
@@ -50,7 +58,7 @@ class CMDBSource
protected static $m_sDBUser;
protected static $m_sDBPwd;
protected static $m_sDBName;
protected static $m_resDBLink;
protected static $m_oMysqli;
public static function Init($sServer, $sUser, $sPwd, $sSource = '')
{
@@ -58,15 +66,41 @@ class CMDBSource
self::$m_sDBUser = $sUser;
self::$m_sDBPwd = $sPwd;
self::$m_sDBName = $sSource;
if (!self::$m_resDBLink = @mysqli_connect($sServer, $sUser, $sPwd))
self::$m_oMysqli = null;
mysqli_report(MYSQLI_REPORT_STRICT); // *some* errors (like connection errors) will throw mysqli_sql_exception instead
// of generating warnings printed to the output but some other errors will still
// cause the query() method to return false !!!
try
{
throw new MySQLException('Could not connect to the DB server', array('host'=>$sServer, 'user'=>$sUser));
$aConnectInfo = explode(':', self::$m_sDBHost);
if (count($aConnectInfo) > 1)
{
// Override the default port
$sServer = $aConnectInfo[0];
$iPort = (int)$aConnectInfo[1];
self::$m_oMysqli = new mysqli(self::$m_sDBHost, self::$m_sDBUser, self::$m_sDBPwd, '', $iPort);
}
else
{
self::$m_oMysqli = new mysqli(self::$m_sDBHost, self::$m_sDBUser, self::$m_sDBPwd);
}
}
catch(mysqli_sql_exception $e)
{
throw new MySQLException('Could not connect to the DB server', array('host'=>self::$m_sDBHost, 'user'=>self::$m_sDBUser), $e);
}
if (!empty($sSource))
{
if (!((bool)mysqli_query(self::$m_resDBLink, "USE $sSource")))
try
{
throw new MySQLException('Could not select DB', array('host'=>$sServer, 'user'=>$sUser, 'db_name'=>$sSource));
mysqli_report(MYSQLI_REPORT_STRICT); // Errors, in the next query, will throw mysqli_sql_exception
self::$m_oMysqli->query("USE `$sSource`");
}
catch(mysqli_sql_exception $e)
{
throw new MySQLException('Could not select DB', array('host'=>self::$m_sDBHost, 'user'=>self::$m_sDBUser, 'db_name'=>self::$m_sDBName), $e);
}
}
}
@@ -120,7 +154,7 @@ class CMDBSource
{
// In case we don't have rights to enumerate the databases
// Let's try to connect directly
return @((bool)mysqli_query(self::$m_resDBLink, "USE $sSource"));
return @((bool)self::$m_oMysqli->query("USE `$sSource`"));
}
}
@@ -133,7 +167,7 @@ class CMDBSource
public static function SelectDB($sSource)
{
if (!((bool)mysqli_query(self::$m_resDBLink, "USE $sSource")))
if (!((bool)self::$m_oMysqli->query("USE `$sSource`")))
{
throw new MySQLException('Could not select DB', array('db_name'=>$sSource));
}
@@ -175,25 +209,25 @@ class CMDBSource
public static function GetErrNo()
{
if (self::$m_resDBLink)
if (self::$m_oMysqli->errno != 0)
{
return mysqli_errno(self::$m_resDBLink);
return self::$m_oMysqli->errno;
}
else
{
return mysqli_connect_errno();
return self::$m_oMysqli->connect_errno;
}
}
public static function GetError()
{
if (self::$m_resDBLink)
if (self::$m_oMysqli->error != '')
{
return mysqli_error(self::$m_resDBLink);
return self::$m_oMysqli->error;
}
else
{
return mysqli_connect_error();
return self::$m_oMysqli->connect_error;
}
}
@@ -234,42 +268,43 @@ class CMDBSource
// Quote if not a number or a numeric string
if ($bAlways || is_string($value))
{
$value = $cQuoteStyle . mysqli_real_escape_string(self::$m_resDBLink, $value) . $cQuoteStyle;
$value = $cQuoteStyle . self::$m_oMysqli->real_escape_string($value) . $cQuoteStyle;
}
return $value;
}
public static function Query($sSQLQuery)
{
// Add info into the query as a comment, for easier error tracking
// disabled until we need it really!
//
//$aTraceInf['file'] = __FILE__;
// $sSQLQuery .= MyHelpers::MakeSQLComment($aTraceInf);
$oKPI = new ExecutionKPI();
$result = mysqli_query(self::$m_resDBLink, $sSQLQuery);
if (!$result)
try
{
$oResult = self::$m_oMysqli->query($sSQLQuery);
}
catch(mysqli_sql_exception $e)
{
throw new MySQLException('Failed to issue SQL query', array('query' => $sSQLQuery, $e));
}
$oKPI->ComputeStats('Query exec (mySQL)', $sSQLQuery);
if ($oResult === false)
{
throw new MySQLException('Failed to issue SQL query', array('query' => $sSQLQuery));
}
$oKPI->ComputeStats('Query exec (mySQL)', $sSQLQuery);
return $result;
return $oResult;
}
public static function GetNextInsertId($sTable)
{
$sSQL = "SHOW TABLE STATUS LIKE '$sTable'";
$result = self::Query($sSQL);
$aRow = mysqli_fetch_assoc($result);
$oResult = self::Query($sSQL);
$aRow = $oResult->fetch_assoc();
$iNextInsertId = $aRow['Auto_increment'];
return $iNextInsertId;
}
public static function GetInsertId()
{
$iRes = mysqli_insert_id(self::$m_resDBLink);
$iRes = self::$m_oMysqli->insert_id;
if (is_null($iRes))
{
return 0;
@@ -293,37 +328,59 @@ class CMDBSource
public static function QueryToScalar($sSql)
{
$result = mysqli_query(self::$m_resDBLink, $sSql);
if (!$result)
$oKPI = new ExecutionKPI();
try
{
$oResult = self::$m_oMysqli->query($sSql);
}
catch(mysqli_sql_exception $e)
{
$oKPI->ComputeStats('Query exec (mySQL)', $sSql);
MySQLException('Failed to issue SQL query', array('query' => $sSql, $e));
}
$oKPI->ComputeStats('Query exec (mySQL)', $sSql);
if ($oResult === false)
{
throw new MySQLException('Failed to issue SQL query', array('query' => $sSql));
}
if ($aRow = mysqli_fetch_array($result, MYSQLI_BOTH))
if ($aRow = $oResult->fetch_array(MYSQLI_BOTH))
{
$res = $aRow[0];
}
else
{
mysqli_free_result($result);
$oResult->free();
throw new MySQLException('Found no result for query', array('query' => $sSql));
}
mysqli_free_result($result);
$oResult->free();
return $res;
}
public static function QueryToArray($sSql)
{
$aData = array();
$result = mysqli_query(self::$m_resDBLink, $sSql);
if (!$result)
$oKPI = new ExecutionKPI();
try
{
$oResult = self::$m_oMysqli->query($sSql);
}
catch(mysqli_sql_exception $e)
{
$oKPI->ComputeStats('Query exec (mySQL)', $sSql);
MySQLException('Failed to issue SQL query', array('query' => $sSql, $e));
}
$oKPI->ComputeStats('Query exec (mySQL)', $sSql);
if ($oResult === false)
{
throw new MySQLException('Failed to issue SQL query', array('query' => $sSql));
}
while ($aRow = mysqli_fetch_array($result, MYSQLI_BOTH))
while ($aRow = $oResult->fetch_array(MYSQLI_BOTH))
{
$aData[] = $aRow;
}
mysqli_free_result($result);
$oResult->free();
return $aData;
}
@@ -341,56 +398,73 @@ class CMDBSource
public static function ExplainQuery($sSql)
{
$aData = array();
$result = mysqli_query(self::$m_resDBLink, "EXPLAIN $sSql");
if (!$result)
try
{
$oResult = self::$m_oMysqli->query($sSql);
}
catch(mysqli_sql_exception $e)
{
MySQLException('Failed to issue SQL query', array('query' => $sSql, $e));
}
if ($oResult === false)
{
throw new MySQLException('Failed to issue SQL query', array('query' => $sSql));
}
$aNames = self::GetColumns($result);
$aNames = self::GetColumns($oResult);
$aData[] = $aNames;
while ($aRow = mysqli_fetch_array($result, MYSQLI_ASSOC))
while ($aRow = $oResult->fetch_array(MYSQLI_ASSOC))
{
$aData[] = $aRow;
}
mysqli_free_result($result);
$oResult->free();
return $aData;
}
public static function TestQuery($sSql)
{
$result = mysqli_query(self::$m_resDBLink, "EXPLAIN $sSql");
if (!$result)
try
{
return self::GetError();
$oResult = self::$m_oMysqli->query($sSql);
}
catch(mysqli_sql_exception $e)
{
MySQLException('Failed to issue SQL query', array('query' => $sSql, $e));
}
if ($oResult === false)
{
throw new MySQLException('Failed to issue SQL query', array('query' => $sSql));
}
mysqli_free_result($result);
if (is_object($oResult))
{
$oResult->free();
}
return '';
}
public static function NbRows($result)
public static function NbRows($oResult)
{
return mysqli_num_rows($result);
return $oResult->num_rows;
}
public static function AffectedRows()
{
return mysqli_affected_rows(self::$m_resDBLink);
return self::$m_oMysqli->affected_rows;
}
public static function FetchArray($result)
public static function FetchArray($oResult)
{
return mysqli_fetch_array($result, MYSQLI_ASSOC);
return $oResult->fetch_array(MYSQLI_ASSOC);
}
public static function GetColumns($result)
public static function GetColumns($oResult)
{
$aNames = array();
for ($i = 0; $i < (($___mysqli_tmp = mysqli_num_fields($result)) ? $___mysqli_tmp : 0) ; $i++)
for ($i = 0; $i < (($___mysqli_tmp = $oResult->field_count) ? $___mysqli_tmp : 0) ; $i++)
{
$meta = mysqli_fetch_field_direct($result, $i);
$meta = $oResult->fetch_field_direct($i);
if (!$meta)
{
throw new MySQLException('mysql_fetch_field: No information available', array('query'=>$sSql, 'i'=>$i));
@@ -403,14 +477,15 @@ class CMDBSource
return $aNames;
}
public static function Seek($result, $iRow)
public static function Seek($oResult, $iRow)
{
return mysqli_data_seek($result, $iRow);
return $oResult->data_seek($iRow);
}
public static function FreeResult($result)
public static function FreeResult($oResult)
{
return ((mysqli_free_result($result) || (is_object($result) && (get_class($result) == "mysqli_result"))) ? true : false);
$oResult->free(); /* returns void */
return true;
}
public static function IsTable($sTable)
@@ -466,14 +541,23 @@ class CMDBSource
return ($aFieldData["Type"]);
}
public static function HasIndex($sTable, $sField)
public static function HasIndex($sTable, $sIndexId, $aFields = null)
{
$aTableInfo = self::GetTableInfo($sTable);
if (empty($aTableInfo)) return false;
if (!array_key_exists($sField, $aTableInfo["Fields"])) return false;
$aFieldData = $aTableInfo["Fields"][$sField];
// $aFieldData could be 'PRI' for the primary key, or 'MUL', or ?
return (strlen($aFieldData["Key"]) > 0);
if (!array_key_exists($sIndexId, $aTableInfo['Indexes'])) return false;
if ($aFields == null)
{
// Just searching for the name
return true;
}
// Compare the columns
$sSearchedIndex = implode(',', $aFields);
$sExistingIndex = implode(',', $aTableInfo['Indexes'][$sIndexId]);
return ($sSearchedIndex == $sExistingIndex);
}
// Returns an array of (fieldname => array of field info)
@@ -523,6 +607,17 @@ class CMDBSource
// Table does not exist
self::$m_aTablesInfo[strtolower($sTableName)] = null;
}
if (!is_null(self::$m_aTablesInfo[strtolower($sTableName)]))
{
$aIndexes = self::QueryToArray("SHOW INDEXES FROM `$sTableName`");
$aMyIndexes = array();
foreach ($aIndexes as $aIndexColumn)
{
$aMyIndexes[$aIndexColumn['Key_name']][$aIndexColumn['Seq_in_index']-1] = $aIndexColumn['Column_name'];
}
self::$m_aTablesInfo[strtolower($sTableName)]["Indexes"] = $aMyIndexes;
}
}
//public static function EnumTables()
//{
@@ -548,18 +643,25 @@ class CMDBSource
public static function DumpTable($sTable)
{
$sSql = "SELECT * FROM `$sTable`";
$result = mysqli_query(self::$m_resDBLink, $sSql);
if (!$result)
try
{
$oResult = self::$m_oMysqli->query($sSql);
}
catch(mysqli_sql_exception $e)
{
throw new MySQLException('Failed to issue SQL query', array('query' => $sSql), $e);
}
if ($oResult === false)
{
throw new MySQLException('Failed to issue SQL query', array('query' => $sSql));
}
$aRows = array();
while ($aRow = mysqli_fetch_array($result, MYSQLI_ASSOC))
while ($aRow = $oResult->fetch_array(MYSQLI_ASSOC))
{
$aRows[] = $aRow;
}
mysqli_free_result($result);
$oResult->free();
return $aRows;
}
@@ -589,7 +691,7 @@ class CMDBSource
{
try
{
$result = self::Query('SHOW GRANTS'); // [ FOR CURRENT_USER()]
$oResult = self::Query('SHOW GRANTS'); // [ FOR CURRENT_USER()]
}
catch(MySQLException $e)
{
@@ -597,12 +699,12 @@ class CMDBSource
}
$aRes = array();
while ($aRow = mysqli_fetch_array($result, MYSQLI_NUM))
while ($aRow = $oResult->fetch_array(MYSQLI_NUM))
{
// so far, only one column...
$aRes[] = implode('/', $aRow);
}
mysqli_free_result($result);
$oResult->free();
// so far, only one line...
return implode(', ', $aRes);
}
@@ -615,21 +717,21 @@ class CMDBSource
{
try
{
$result = self::Query('SHOW SLAVE STATUS');
$oResult = self::Query('SHOW SLAVE STATUS');
}
catch(MySQLException $e)
{
throw new CoreException("Current user not allowed to check the status", array('mysql_error' => $e->getMessage()));
}
if (mysqli_num_rows($result) == 0)
if ($oResult->num_rows == 0)
{
return false;
}
// Returns one single row anytime
$aRow = mysqli_fetch_array($result, MYSQLI_ASSOC);
mysqli_free_result($result);
$aRow = $oResult->fetch_array(MYSQLI_ASSOC);
$oResult->free();
if (!isset($aRow['Slave_IO_Running']))
{
@@ -651,7 +753,4 @@ class CMDBSource
}
return false;
}
}
?>
}

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -35,6 +35,7 @@ define('ACCESS_READONLY', 0);
*/
require_once('coreexception.class.inc.php');
require_once('attributedef.class.inc.php'); // For the defines
class ConfigException extends CoreException
{
@@ -47,8 +48,6 @@ define ('DEFAULT_LOG_GLOBAL', true);
define ('DEFAULT_LOG_NOTIFICATION', true);
define ('DEFAULT_LOG_ISSUE', true);
define ('DEFAULT_LOG_WEB_SERVICE', true);
define ('DEFAULT_LOG_KPI_DURATION', false);
define ('DEFAULT_LOG_KPI_MEMORY', false);
define ('DEFAULT_LOG_QUERIES', false);
define ('DEFAULT_QUERY_CACHE_ENABLED', true);
@@ -149,6 +148,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'disable_mandatory_ext_keys' => array(
'type' => 'bool',
'description' => 'For developpers: allow every external keys to be undefined',
'default' => false,
'value' => false,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'graphviz_path' => array(
'type' => 'string',
'description' => 'Path to the Graphviz "dot" executable for graphing objects lifecycle',
@@ -326,6 +333,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'async_task_retries' => array(
'type' => 'array',
'description' => 'Automatic retries of asynchronous tasks in case of failure (per class)',
'default' => array('AsyncSendEmail' => array('max_retries' => 0, 'retry_delay' => 600)),
'value' => false,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'email_asynchronous' => array(
'type' => 'bool',
'description' => 'If set, the emails are sent off line, which requires cron.php to be activated. Exception: some features like the email test utility will force the serialized mode',
@@ -525,6 +540,24 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'forgot_password' => array(
'type' => 'bool',
'description' => 'Enable the "Forgot password" feature',
// examples... not used (nor 'description')
'default' => true,
'value' => true,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'forgot_password_from' => array(
'type' => 'string',
'description' => 'Sender email address for the "forgot password" feature. If empty, defaults to the recipient\'s email address.',
// examples... not used (nor 'description')
'default' => '',
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'deadline_format' => array(
'type' => 'string',
'description' => 'The format used for displaying "deadline" attributes: any string with the following placeholders: $date$, $difference$',
@@ -606,6 +639,136 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'url_validation_pattern' => array(
'type' => 'string',
'description' => 'Regular expression to validate/detect the format of an URL (URL attributes and Wiki formatting for Text attributes)',
'default' => '(https?|ftp)\://([a-zA-Z0-9+!*(),;?&=\$_.-]+(\:[a-zA-Z0-9+!*(),;?&=\$_.-]+)?@)?([a-zA-Z0-9-.]{3,})(\:[0-9]{2,5})?(/([a-zA-Z0-9%+\$_-]\.?)+)*/?(\?[a-zA-Z+&\$_.-][a-zA-Z0-9;:[\]@&%=+/\$_.-]*)?(#[a-zA-Z_.-][a-zA-Z0-9+\$_.-]*)?',
// SHEME.......... USER....................... PASSWORD.......................... HOST/IP........... PORT.......... PATH........................ GET............................................ ANCHOR............................
// Example: http://User:passWord@127.0.0.1:8888/patH/Page.php?arrayArgument[2]=something:blah20#myAnchor
// Origin of this regexp: http://www.php.net/manual/fr/function.preg-match.php#93824
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => true,
),
'email_validation_pattern' => array(
'type' => 'string',
'description' => 'Regular expression to validate/detect the format of an eMail address',
'default' => "[a-zA-Z0-9._&'-]+@[a-zA-Z0-9.-]+\.[a-zA-Z0-9-]{2,}",
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => true,
),
'log_kpi_duration' => array(
'type' => 'integer',
'description' => 'Level of logging for troubleshooting performance issues (1 to enable, 2 +blame callers)',
// examples... not used
'default' => 0,
'value' => 0,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'log_kpi_memory' => array(
'type' => 'integer',
'description' => 'Level of logging for troubleshooting memory limit issues',
// examples... not used
'default' => 0,
'value' => 0,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'log_kpi_user_id' => array(
'type' => 'string',
'description' => 'Limit the scope of users to the given user id (* means no limit)',
// examples... not used
'default' => '*',
'value' => '*',
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'max_linkset_output' => array(
'type' => 'integer',
'description' => 'Maximum number of items shown when getting a list of related items in an email, using the form $this->some_list$. 0 means no limit.',
'default' => 100,
'value' => 100,
'source_of_value' => '',
'show_in_conf_sample' => true,
),
'demo_mode' => array(
'type' => 'bool',
'description' => 'Set to true to prevent users from changing passwords/languages',
'default' => false,
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'portal_tickets' => array(
'type' => 'string',
'description' => 'CSV list of classes supported in the portal',
// examples... not used
'default' => 'UserRequest',
'value' => 'UserRequest',
'source_of_value' => '',
'show_in_conf_sample' => true,
),
'max_execution_time_per_loop' => array(
'type' => 'integer',
'description' => 'Maximum execution time requested, per loop, during bulk operations. Zero means no limit.',
// examples... not used
'default' => 30,
'value' => 30,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'max_history_length' => array(
'type' => 'integer',
'description' => 'Maximum length of the history table (in the "History" tab on each object) before it gets truncated. Latest modifications are displayed first.',
// examples... not used
'default' => 50,
'value' => 50,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'full_text_chunk_duration' => array(
'type' => 'integer',
'description' => 'Delay after which the results are displayed.',
// examples... not used
'default' => 2,
'value' => 2,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'full_text_accelerators' => array(
'type' => 'array',
'description' => 'Specifies classes to be searched at first (and the subset of data) when running the full text search.',
'default' => array(),
'value' => false,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'full_text_needle_min' => array(
'type' => 'integer',
'description' => 'Minimum size of the full text needle.',
'default' => 3,
'value' => 3,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'tracking_level_linked_set_default' => array(
'type' => 'integer',
'description' => 'Default tracking level if not explicitely set at the attribute level, for AttributeLinkedSet (defaults to NONE in case of a fresh install, LIST otherwise - this to preserve backward compatibility while upgrading from a version older than 2.0.3 - see TRAC #936)',
'default' => LINKSET_TRACKING_LIST,
'value' => LINKSET_TRACKING_LIST,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'tracking_level_linked_set_indirect_default' => array(
'type' => 'integer',
'description' => 'Default tracking level if not explicitely set at the attribute level, for AttributeLinkedSetIndirect',
'default' => LINKSET_TRACKING_ALL,
'value' => LINKSET_TRACKING_ALL,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
);
public function IsProperty($sPropCode)
@@ -634,6 +797,8 @@ class Config
case 'float':
$value = (float) $value;
break;
case 'array':
break;
default:
throw new CoreException('Unknown type for setting', array('property' => $sPropCode, 'type' => $sType));
}
@@ -664,8 +829,6 @@ class Config
protected $m_bLogNotification;
protected $m_bLogIssue;
protected $m_bLogWebService;
protected $m_bLogKPIDuration; // private setting
protected $m_bLogKPIMemory; // private setting
protected $m_bLogQueries; // private setting
protected $m_bQueryCacheEnabled; // private setting
@@ -747,6 +910,7 @@ class Config
'core/action.class.inc.php',
'core/trigger.class.inc.php',
'synchro/synchrodatasource.class.inc.php',
'core/backgroundtask.class.inc.php',
);
$this->m_aDataModels = array();
$this->m_aWebServiceCategories = array(
@@ -774,8 +938,6 @@ class Config
$this->m_bLogNotification = DEFAULT_LOG_NOTIFICATION;
$this->m_bLogIssue = DEFAULT_LOG_ISSUE;
$this->m_bLogWebService = DEFAULT_LOG_WEB_SERVICE;
$this->m_bLogKPIDuration = DEFAULT_LOG_KPI_DURATION;
$this->m_bLogKPIDuration = DEFAULT_LOG_KPI_DURATION;
$this->m_iMinDisplayLimit = DEFAULT_MIN_DISPLAY_LIMIT;
$this->m_iMaxDisplayLimit = DEFAULT_MAX_DISPLAY_LIMIT;
$this->m_iStandardReloadInterval = DEFAULT_STANDARD_RELOAD_INTERVAL;
@@ -893,7 +1055,14 @@ class Config
{
if ($this->IsProperty($sPropCode))
{
$value = trim($rawvalue);
if (is_string($rawvalue))
{
$value = trim($rawvalue);
}
else
{
$value = $rawvalue;
}
$this->Set($sPropCode, $value, $sConfigFile);
}
}
@@ -911,8 +1080,6 @@ class Config
$this->m_bLogNotification = isset($MySettings['log_notification']) ? (bool) trim($MySettings['log_notification']) : DEFAULT_LOG_NOTIFICATION;
$this->m_bLogIssue = isset($MySettings['log_issue']) ? (bool) trim($MySettings['log_issue']) : DEFAULT_LOG_ISSUE;
$this->m_bLogWebService = isset($MySettings['log_web_service']) ? (bool) trim($MySettings['log_web_service']) : DEFAULT_LOG_WEB_SERVICE;
$this->m_bLogKPIDuration = isset($MySettings['log_kpi_duration']) ? (bool) trim($MySettings['log_kpi_duration']) : DEFAULT_LOG_KPI_DURATION;
$this->m_bLogKPIMemory = isset($MySettings['log_kpi_memory']) ? (bool) trim($MySettings['log_kpi_memory']) : DEFAULT_LOG_KPI_MEMORY;
$this->m_bLogQueries = isset($MySettings['log_queries']) ? (bool) trim($MySettings['log_queries']) : DEFAULT_LOG_QUERIES;
$this->m_bQueryCacheEnabled = isset($MySettings['query_cache_enabled']) ? (bool) trim($MySettings['query_cache_enabled']) : DEFAULT_QUERY_CACHE_ENABLED;
@@ -1051,16 +1218,6 @@ class Config
return $this->m_bLogWebService;
}
public function GetLogKPIDuration()
{
return $this->m_bLogKPIDuration;
}
public function GetLogKPIMemory()
{
return $this->m_bLogKPIMemory;
}
public function GetLogQueries()
{
return $this->m_bLogQueries;
@@ -1569,10 +1726,6 @@ class Config
{
$aWebServiceCategories = array_unique(array_merge($aWebServiceCategories, $aModuleInfo['webservice']));
}
if (isset($aModuleInfo['dictionary']))
{
$aDictionaries = array_unique(array_merge($aDictionaries, $aModuleInfo['dictionary']));
}
if (isset($aModuleInfo['settings']))
{
list($sName, $sVersion) = ModuleDiscovery::GetModuleName($sModuleId);
@@ -1608,6 +1761,17 @@ class Config
$this->SetAppModules($aAppModules);
$this->SetDataModels($aDataModels);
$this->SetWebServiceCategories($aWebServiceCategories);
// Scan dictionaries
//
if (!is_null($sModulesDir))
{
foreach(glob(APPROOT.$sModulesDir.'/dictionaries/*.dict.php') as $sFilePath)
{
$sFile = basename($sFilePath);
$aDictionaries[] = $sModulesDir.'/dictionaries/'.$sFile;
}
}
$this->SetDictionaries($aDictionaries);
}
}
@@ -1641,14 +1805,14 @@ class Config
/**
* Pretty format a var_export'ed value so that (if possible) the identation is preserved on every line
* @param mixed $value The value to export
* @param string $sIdentation The string to use to indent the text
* @param string $sIndentation The string to use to indent the text
* @param bool $bForceIndentation Forces the identation (enven if it breaks/changes an eval, for example to ouput a value inside a comment)
* @return string The indented export string
*/
protected static function PrettyVarExport($value, $sIdentation, $bForceIndentation = false)
protected static function PrettyVarExport($value, $sIndentation, $bForceIndentation = false)
{
$sExport = var_export($value, true);
$sNiceExport = trim(preg_replace("/^/m", "\t\t\t", $sExport));
$sNiceExport = str_replace(array("\r\n", "\n", "\r"), "\n".$sIndentation, trim($sExport));
if (!$bForceIndentation)
{
eval('$aImported='.$sNiceExport.';');

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2014 Combodo SARL
//
// This file is part of iTop.
//
@@ -16,6 +16,38 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* All objects to be displayed in the application (either as a list or as details)
* must implement this interface.
*/
interface iDisplay
{
/**
* Maps the given context parameter name to the appropriate filter/search code for this class
* @param string $sContextParam Name of the context parameter, i.e. 'org_id'
* @return string Filter code, i.e. 'customer_id'
*/
public static function MapContextParam($sContextParam);
/**
* This function returns a 'hilight' CSS class, used to hilight a given row in a table
* There are currently (i.e defined in the CSS) 4 possible values HILIGHT_CLASS_CRITICAL,
* HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE
* To Be overridden by derived classes
* @param void
* @return String The desired higlight class for the object/row
*/
public function GetHilightClass();
/**
* Returns the relative path to the page that handles the display of the object
* @return string
*/
public static function GetUIPage();
/**
* Displays the details of the object
*/
public function DisplayDetails(WebPage $oPage, $bEditMode = false);
}
/**
* Class dbObject: the root of persistent classes
@@ -26,6 +58,7 @@
require_once('metamodel.class.php');
require_once('deletionplan.class.inc.php');
require_once('mutex.class.inc.php');
/**
@@ -33,7 +66,7 @@ require_once('deletionplan.class.inc.php');
*
* @package iTopORM
*/
abstract class DBObject
abstract class DBObject implements iDisplay
{
private static $m_aMemoryObjectsByClass = array();
@@ -60,6 +93,7 @@ abstract class DBObject
private $m_bFullyLoaded = false; // Compound objects can be partially loaded
private $m_aLoadedAtt = array(); // Compound objects can be partially loaded, array of sAttCode
protected $m_aModifiedAtt = array(); // list of (potentially) modified sAttCodes
protected $m_oMasterReplicaSet = null; // Set of SynchroReplica related to this object
// Use the MetaModel::NewObject to build an object (do we have to force it?)
@@ -69,6 +103,7 @@ abstract class DBObject
{
$this->FromRow($aRow, $sClassAlias, $aAttToLoad, $aExtendedDataSpec);
$this->m_bFullyLoaded = $this->IsFullyLoaded();
$this->m_aModifiedAtt = array();
return;
}
// Creation of a brand new object
@@ -193,6 +228,7 @@ abstract class DBObject
}
$this->m_bFullyLoaded = true;
$this->m_aModifiedAtt = array();
}
protected function FromRow($aRow, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null)
@@ -303,6 +339,7 @@ abstract class DBObject
// Ignore it - this attribute is set upon object creation and that's it
return;
}
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
if ($this->m_bIsInDB && !$this->m_bFullyLoaded && !$this->m_bDirty)
{
@@ -370,6 +407,7 @@ abstract class DBObject
$realvalue = $oAttDef->MakeRealValue($value, $this);
$this->m_aCurrValues[$sAttCode] = $realvalue;
$this->m_aModifiedAtt[$sAttCode] = true;
// The object has changed, reset caches
$this->m_bCheckStatus = null;
@@ -399,16 +437,25 @@ abstract class DBObject
{
throw new CoreException("Unknown external key '$sExtKeyAttCode' for the class ".get_class($this));
}
$oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode);
$sRemoteClass = $oKeyAttDef->GetTargetClass();
$oRemoteObj = MetaModel::GetObject($sRemoteClass, $this->GetStrict($sExtKeyAttCode), false);
if (is_null($oRemoteObj))
$oExtFieldAtt = MetaModel::FindExternalField(get_class($this), $sExtKeyAttCode, $sRemoteAttCode);
if (!is_null($oExtFieldAtt))
{
return '';
return $this->GetStrict($oExtFieldAtt->GetCode());
}
else
{
return $oRemoteObj->Get($sRemoteAttCode);
$oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode);
$sRemoteClass = $oKeyAttDef->GetTargetClass();
$oRemoteObj = MetaModel::GetObject($sRemoteClass, $this->GetStrict($sExtKeyAttCode), false);
if (is_null($oRemoteObj))
{
return '';
}
else
{
return $oRemoteObj->Get($sRemoteAttCode);
}
}
}
}
@@ -433,7 +480,9 @@ abstract class DBObject
{
// Lazy load (polymorphism): complete by reloading the entire object
// #@# non-scalar attributes.... handle that differently?
$oKPI = new ExecutionKPI();
$this->Reload();
$oKPI->ComputeStats('Reload', get_class($this).'/'.$sAttCode);
}
elseif ($sAttCode == 'friendlyname')
{
@@ -455,7 +504,10 @@ abstract class DBObject
if (($iRemote = $this->Get($sExtKeyAttCode)) && ($iRemote > 0)) // Objects in memory have negative IDs
{
$oExtKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode);
$oRemote = MetaModel::GetObject($oExtKeyAttDef->GetTargetClass(), $iRemote);
// Note: "allow all data" must be enabled because the external fields are always visible
// to the current user even if this is not the case for the remote object
// This is consistent with the behavior of the lists
$oRemote = MetaModel::GetObject($oExtKeyAttDef->GetTargetClass(), $iRemote, true, true);
}
else
{
@@ -509,6 +561,9 @@ abstract class DBObject
/**
* Updates the value of an external field by (re)loading the object
* corresponding to the external key and getting the value from it
*
* UNUSED ?
*
* @param string $sAttCode Attribute code of the external field to update
* @return void
*/
@@ -519,7 +574,10 @@ abstract class DBObject
{
$sTargetClass = $oAttDef->GetTargetClass();
$objkey = $this->Get($oAttDef->GetKeyAttCode());
$oObj = MetaModel::GetObject($sTargetClass, $objkey);
// Note: "allow all data" must be enabled because the external fields are always visible
// to the current user even if this is not the case for the remote object
// This is consistent with the behavior of the lists
$oObj = MetaModel::GetObject($sTargetClass, $objkey, true, true);
if (is_object($oObj))
{
$value = $oObj->Get($oAttDef->GetExtAttCode());
@@ -852,7 +910,11 @@ abstract class DBObject
}
}
$aReasons = array();
$iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons);
$iSynchroFlags = 0;
if ($this->InSyncScope())
{
$iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons);
}
return $iFlags | $iSynchroFlags; // Combine both sets of flags
}
@@ -925,7 +987,7 @@ abstract class DBObject
}
elseif ($oAtt->IsScalar())
{
$aValues = $oAtt->GetAllowedValues($this->ToArgs());
$aValues = $oAtt->GetAllowedValues($this->ToArgsForQuery());
if (count($aValues) > 0)
{
if (!array_key_exists($toCheck, $aValues))
@@ -961,7 +1023,6 @@ abstract class DBObject
{
$this->DoComputeValues();
$this->m_aCheckIssues = array();
$aChanges = $this->ListChanges();
foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef)
@@ -1016,6 +1077,8 @@ abstract class DBObject
}
if (is_null($this->m_bCheckStatus))
{
$this->m_aCheckIssues = array();
$oKPI = new ExecutionKPI();
$this->DoCheckToWrite();
$oKPI->ComputeStats('CheckToWrite', get_class($this));
@@ -1103,6 +1166,11 @@ abstract class DBObject
// The value was not set
$aDelta[$sAtt] = $proposedValue;
}
elseif(!array_key_exists($sAtt, $this->m_aModifiedAtt))
{
// This attCode was never set, canno tbe modified
continue;
}
elseif(is_object($proposedValue))
{
$oLinkAttDef = MetaModel::GetAttributeDef(get_class($this), $sAtt);
@@ -1190,24 +1258,39 @@ abstract class DBObject
foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef)
{
if (!$oAttDef->IsLinkSet()) continue;
$oOriginalSet = $this->m_aOrigValues[$sAttCode];
if ($oOriginalSet != null)
{
$aOriginalList = $oOriginalSet->ToArray();
}
else
{
$aOriginalList = array();
}
$oLinks = $this->Get($sAttCode);
$oLinks->Rewind();
while ($oLinkedObject = $oLinks->Fetch())
{
$oLinkedObject->Set($oAttDef->GetExtKeyToMe(), $this->m_iKey);
if (!array_key_exists($oLinkedObject->GetKey(), $aOriginalList))
{
// New object added to the set, make it point properly
$oLinkedObject->Set($oAttDef->GetExtKeyToMe(), $this->m_iKey);
}
if ($oLinkedObject->IsModified())
{
// Objects can be modified because:
// 1) They've just been added into the set, so their ExtKey is modified
// 2) They are about to be removed from the set BUT NOT deleted, their ExtKey has been reset
$oLinkedObject->DBWrite();
}
}
// Delete the objects that were initialy present and disappeared from the list
// (if any)
$oOriginalSet = $this->m_aOrigValues[$sAttCode];
if ($oOriginalSet != null)
if (count($aOriginalList) > 0)
{
$aOriginalList = $oOriginalSet->ToArray();
$aNewSet = $oLinks->ToArray();
foreach($aOriginalList as $iId => $oObject)
@@ -1430,6 +1513,95 @@ abstract class DBObject
return $this->m_iKey;
}
protected function MakeInsertStatementSingleTable($aAuthorizedExtKeys, &$aStatements, $sTableClass)
{
$sTable = MetaModel::DBGetTable($sTableClass);
// Abstract classes or classes having no specific attribute do not have an associated table
if ($sTable == '') return;
$sClass = get_class($this);
// fields in first array, values in the second
$aFieldsToWrite = array();
$aValuesToWrite = array();
if (!empty($this->m_iKey) && ($this->m_iKey >= 0))
{
// Add it to the list of fields to write
$aFieldsToWrite[] = '`'.MetaModel::DBGetKey($sTableClass).'`';
$aValuesToWrite[] = CMDBSource::Quote($this->m_iKey);
}
$aHierarchicalKeys = array();
foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef)
{
// Skip this attribute if not defined in this table
if (!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode)) continue;
// Skip link set that can still be undefined though the object is 100% loaded
if ($oAttDef->IsLinkSet()) continue;
$value = $this->m_aCurrValues[$sAttCode];
if ($oAttDef->IsExternalKey())
{
$sTargetClass = $oAttDef->GetTargetClass();
if (is_array($aAuthorizedExtKeys))
{
if (!array_key_exists($sTargetClass, $aAuthorizedExtKeys) || !array_key_exists($value, $aAuthorizedExtKeys[$sTargetClass]))
{
$value = 0;
}
}
}
$aAttColumns = $oAttDef->GetSQLValues($value);
foreach($aAttColumns as $sColumn => $sValue)
{
$aFieldsToWrite[] = "`$sColumn`";
$aValuesToWrite[] = CMDBSource::Quote($sValue);
}
if ($oAttDef->IsHierarchicalKey())
{
$aHierarchicalKeys[$sAttCode] = $oAttDef;
}
}
if (count($aValuesToWrite) == 0) return false;
if (count($aHierarchicalKeys) > 0)
{
foreach($aHierarchicalKeys as $sAttCode => $oAttDef)
{
$aValues = MetaModel::HKInsertChildUnder($this->m_aCurrValues[$sAttCode], $oAttDef, $sTable);
$aFieldsToWrite[] = '`'.$oAttDef->GetSQLRight().'`';
$aValuesToWrite[] = $aValues[$oAttDef->GetSQLRight()];
$aFieldsToWrite[] = '`'.$oAttDef->GetSQLLeft().'`';
$aValuesToWrite[] = $aValues[$oAttDef->GetSQLLeft()];
}
}
$aStatements[] = "INSERT INTO `$sTable` (".join(",", $aFieldsToWrite).") VALUES (".join(", ", $aValuesToWrite).");";
}
public function MakeInsertStatements($aAuthorizedExtKeys, &$aStatements)
{
$sClass = get_class($this);
$sRootClass = MetaModel::GetRootClass($sClass);
// First query built upon on the root class, because the ID must be created first
$this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sRootClass);
// Then do the leaf class, if different from the root class
if ($sClass != $sRootClass)
{
$this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sClass);
}
// Then do the other classes
foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass)
{
if ($sParentClass == $sRootClass) continue;
$this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sParentClass);
}
}
public function DBInsert()
{
$this->DBInsertNoReload();
@@ -1480,142 +1652,161 @@ abstract class DBObject
throw new CoreException("DBUpdate: could not update a newly created object, please call DBInsert instead");
}
// Stop watches
$sState = $this->GetState();
if ($sState != '')
// Protect against reentrance (e.g. cascading the update of ticket logs)
static $aUpdateReentrance = array();
$sKey = get_class($this).'::'.$this->GetKey();
if (array_key_exists($sKey, $aUpdateReentrance))
{
foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
return;
}
$aUpdateReentrance[$sKey] = true;
try
{
// Stop watches
$sState = $this->GetState();
if ($sState != '')
{
if ($oAttDef instanceof AttributeStopWatch)
foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
{
if (in_array($sState, $oAttDef->GetStates()))
if ($oAttDef instanceof AttributeStopWatch)
{
// Compute or recompute the deadlines
$oSW = $this->Get($sAttCode);
$oSW->ComputeDeadlines($this, $oAttDef);
$this->Set($sAttCode, $oSW);
if (in_array($sState, $oAttDef->GetStates()))
{
// Compute or recompute the deadlines
$oSW = $this->Get($sAttCode);
$oSW->ComputeDeadlines($this, $oAttDef);
$this->Set($sAttCode, $oSW);
}
}
}
}
}
$this->DoComputeValues();
$this->OnUpdate();
$this->DoComputeValues();
$this->OnUpdate();
$aChanges = $this->ListChanges();
if (count($aChanges) == 0)
{
// Attempting to update an unchanged object
return;
}
// Ultimate check - ensure DB integrity
list($bRes, $aIssues) = $this->CheckToWrite();
if (!$bRes)
{
$sIssues = implode(', ', $aIssues);
throw new CoreException("Object not following integrity rules", array('issues' => $sIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
}
// Save the original values (will be reset to the new values when the object get written to the DB)
$aOriginalValues = $this->m_aOrigValues;
$bHasANewExternalKeyValue = false;
$aHierarchicalKeys = array();
foreach($aChanges as $sAttCode => $valuecurr)
{
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
if ($oAttDef->IsExternalKey()) $bHasANewExternalKeyValue = true;
if (!$oAttDef->IsDirectField()) unset($aChanges[$sAttCode]);
if ($oAttDef->IsHierarchicalKey())
$aChanges = $this->ListChanges();
if (count($aChanges) == 0)
{
$aHierarchicalKeys[$sAttCode] = $oAttDef;
// Attempting to update an unchanged object
unset($aUpdateReentrance[$sKey]);
return $this->m_iKey;
}
}
if (!MetaModel::DBIsReadOnly())
{
// Update the left & right indexes for each hierarchical key
foreach($aHierarchicalKeys as $sAttCode => $oAttDef)
// Ultimate check - ensure DB integrity
list($bRes, $aIssues) = $this->CheckToWrite();
if (!$bRes)
{
$sTable = $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode);
$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".$this->GetKey();
$aRes = CMDBSource::QueryToArray($sSQL);
$iMyLeft = $aRes[0]['left'];
$iMyRight = $aRes[0]['right'];
$iDelta =$iMyRight - $iMyLeft + 1;
MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable);
if ($aChanges[$sAttCode] == 0)
$sIssues = implode(', ', $aIssues);
throw new CoreException("Object not following integrity rules", array('issues' => $sIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
}
// Save the original values (will be reset to the new values when the object get written to the DB)
$aOriginalValues = $this->m_aOrigValues;
$bHasANewExternalKeyValue = false;
$aHierarchicalKeys = array();
foreach($aChanges as $sAttCode => $valuecurr)
{
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
if ($oAttDef->IsExternalKey()) $bHasANewExternalKeyValue = true;
if (!$oAttDef->IsDirectField()) unset($aChanges[$sAttCode]);
if ($oAttDef->IsHierarchicalKey())
{
// No new parent, insert completely at the right of the tree
$sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`";
$aHierarchicalKeys[$sAttCode] = $oAttDef;
}
}
if (!MetaModel::DBIsReadOnly())
{
// Update the left & right indexes for each hierarchical key
foreach($aHierarchicalKeys as $sAttCode => $oAttDef)
{
$sTable = $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode);
$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".$this->GetKey();
$aRes = CMDBSource::QueryToArray($sSQL);
if (count($aRes) == 0)
$iMyLeft = $aRes[0]['left'];
$iMyRight = $aRes[0]['right'];
$iDelta =$iMyRight - $iMyLeft + 1;
MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable);
if ($aChanges[$sAttCode] == 0)
{
$iNewLeft = 1;
// No new parent, insert completely at the right of the tree
$sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`";
$aRes = CMDBSource::QueryToArray($sSQL);
if (count($aRes) == 0)
{
$iNewLeft = 1;
}
else
{
$iNewLeft = $aRes[0]['max']+1;
}
}
else
{
$iNewLeft = $aRes[0]['max']+1;
// Insert at the right of the specified parent
$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` FROM `$sTable` WHERE id=".((int)$aChanges[$sAttCode]);
$iNewLeft = CMDBSource::QueryToScalar($sSQL);
}
MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable);
$aHKChanges = array();
$aHKChanges[$sAttCode] = $aChanges[$sAttCode];
$aHKChanges[$oAttDef->GetSQLLeft()] = $iNewLeft;
$aHKChanges[$oAttDef->GetSQLRight()] = $iNewLeft + $iDelta - 1;
$aChanges[$sAttCode] = $aHKChanges; // the 3 values will be stored by MakeUpdateQuery below
}
// Update scalar attributes
if (count($aChanges) != 0)
{
$oFilter = new DBObjectSearch(get_class($this));
$oFilter->AddCondition('id', $this->m_iKey, '=');
$sSQL = MetaModel::MakeUpdateQuery($oFilter, $aChanges);
CMDBSource::Query($sSQL);
}
}
$this->DBWriteLinks();
$this->m_bDirty = false;
$this->AfterUpdate();
// Reload to get the external attributes
if ($bHasANewExternalKeyValue)
{
$this->Reload();
}
else
{
// Reset original values although the object has not been reloaded
foreach ($this->m_aLoadedAtt as $sAttCode => $bLoaded)
{
if ($bLoaded)
{
$value = $this->m_aCurrValues[$sAttCode];
$this->m_aOrigValues[$sAttCode] = is_object($value) ? clone $value : $value;
}
}
else
{
// Insert at the right of the specified parent
$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` FROM `$sTable` WHERE id=".((int)$aChanges[$sAttCode]);
$iNewLeft = CMDBSource::QueryToScalar($sSQL);
}
MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable);
$aHKChanges = array();
$aHKChanges[$sAttCode] = $aChanges[$sAttCode];
$aHKChanges[$oAttDef->GetSQLLeft()] = $iNewLeft;
$aHKChanges[$oAttDef->GetSQLRight()] = $iNewLeft + $iDelta - 1;
$aChanges[$sAttCode] = $aHKChanges; // the 3 values will be stored by MakeUpdateQuery below
}
// Update scalar attributes
}
if (count($aChanges) != 0)
{
$oFilter = new DBObjectSearch(get_class($this));
$oFilter->AddCondition('id', $this->m_iKey, '=');
$sSQL = MetaModel::MakeUpdateQuery($oFilter, $aChanges);
CMDBSource::Query($sSQL);
$this->RecordAttChanges($aChanges, $aOriginalValues);
}
}
$this->DBWriteLinks();
$this->m_bDirty = false;
$this->AfterUpdate();
// Reload to get the external attributes
if ($bHasANewExternalKeyValue)
catch (Exception $e)
{
$this->Reload();
}
else
{
// Reset original values although the object has not been reloaded
foreach ($this->m_aLoadedAtt as $sAttCode => $bLoaded)
{
if ($bLoaded)
{
$value = $this->m_aCurrValues[$sAttCode];
$this->m_aOrigValues[$sAttCode] = is_object($value) ? clone $value : $value;
}
}
}
if (count($aChanges) != 0)
{
$this->RecordAttChanges($aChanges, $aOriginalValues);
unset($aUpdateReentrance[$sKey]);
throw $e;
}
unset($aUpdateReentrance[$sKey]);
return $this->m_iKey;
}
@@ -1694,7 +1885,8 @@ abstract class DBObject
$this->AfterDelete();
$this->m_bIsInDB = false;
$this->m_iKey = null;
// Fix for #926: do NOT reset m_iKey as it can be used to have it for reporting purposes (see the REST service to delete objects, reported as bug #926)
// Thought the key is not reset, using DBInsert or DBWrite will create an object having the same characteristics and a new ID. DBUpdate is protected
}
}
@@ -1702,6 +1894,11 @@ abstract class DBObject
//
public function DBDelete(&$oDeletionPlan = null)
{
static $iLoopTimeLimit = null;
if ($iLoopTimeLimit == null)
{
$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
}
if (is_null($oDeletionPlan))
{
$oDeletionPlan = new DeletionPlan();
@@ -1716,6 +1913,10 @@ abstract class DBObject
}
else
{
// Getting and setting time limit are not symetric:
// www.php.net/manual/fr/function.set-time-limit.php#72305
$iPreviousTimeLimit = ini_get('max_execution_time');
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aToDelete)
{
foreach ($aToDelete as $iId => $aData)
@@ -1727,6 +1928,7 @@ abstract class DBObject
// As a temporary fix: delete only the objects that are still to be deleted...
if ($oToDelete->m_bIsInDB)
{
set_time_limit($iLoopTimeLimit);
$oToDelete->DBDeleteSingleObject();
}
}
@@ -1740,10 +1942,13 @@ abstract class DBObject
foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef)
{
$oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]);
set_time_limit($iLoopTimeLimit);
$oToUpdate->DBUpdate();
}
}
}
set_time_limit($iPreviousTimeLimit);
}
return $oDeletionPlan;
@@ -1776,6 +1981,11 @@ abstract class DBObject
MyHelpers::CheckKeyInArray('object lifecycle stimulus', $sStimulusCode, MetaModel::EnumStimuli(get_class($this)));
$aStateTransitions = $this->EnumTransitions();
if (!array_key_exists($sStimulusCode, $aStateTransitions))
{
// This simulus has no effect in the current state... do nothing
return;
}
$aTransitionDef = $aStateTransitions[$sStimulusCode];
// Change the state before proceeding to the actions, this is necessary because an action might
@@ -1862,20 +2072,31 @@ abstract class DBObject
$this->Set($sAttCode, $oSW);
}
// Make standard context arguments
// Note: Needs to be reviewed because it is currently called once per attribute when an object is written (CheckToWrite / CheckValue)
// Several options here:
// 1) cache the result
// 2) set only the object ref and resolve the values iif needed from contextual templates and queries (easy for the queries, not for the templates)
/*
* Create query parameters (SELECT ... WHERE service = :this->service_id)
* to be used with the APIs DBObjectSearch/DBObjectSet
*
* Starting 2.0.2 the parameters are computed on demand, at the lowest level,
* in VariableExpression::Render()
*/
public function ToArgsForQuery($sArgName = 'this')
{
return array($sArgName.'->object()' => $this);
}
/*
* Create template placeholders
* An improvement could be to compute the values on demand
* (i.e. interpret the template to determine the placeholders)
*/
public function ToArgs($sArgName = 'this')
{
if (is_null($this->m_aAsArgs))
{
$oKPI = new ExecutionKPI();
$aScalarArgs = array();
$aScalarArgs = $this->ToArgsForQuery($sArgName);
$aScalarArgs[$sArgName] = $this->GetKey();
$aScalarArgs[$sArgName.'->id'] = $this->GetKey();
$aScalarArgs[$sArgName.'->object()'] = $this;
$aScalarArgs[$sArgName.'->hyperlink()'] = $this->GetHyperlink('iTopStandardURLMaker', false);
$aScalarArgs[$sArgName.'->hyperlink(portal)'] = $this->GetHyperlink('PortalURLMaker', false);
$aScalarArgs[$sArgName.'->name()'] = $this->GetName();
@@ -1883,20 +2104,45 @@ abstract class DBObject
$sClass = get_class($this);
foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
{
$aScalarArgs[$sArgName.'->'.$sAttCode] = $this->Get($sAttCode);
if ($oAttDef->IsScalar())
if ($oAttDef instanceof AttributeCaseLog)
{
$oCaseLog = $this->Get($sAttCode);
$aScalarArgs[$sArgName.'->'.$sAttCode] = $oCaseLog->GetText();
$sHead = $oCaseLog->GetLatestEntry();
$aScalarArgs[$sArgName.'->head('.$sAttCode.')'] = $sHead;
$aScalarArgs[$sArgName.'->head_html('.$sAttCode.')'] = '<div class="caselog_entry">'.str_replace(array("\r\n", "\n", "\r"), "<br/>", htmlentities($sHead, ENT_QUOTES, 'UTF-8')).'</div>';
$aScalarArgs[$sArgName.'->html('.$sAttCode.')'] = $oCaseLog->GetAsEmailHtml();
}
elseif ($oAttDef->IsScalar())
{
$aScalarArgs[$sArgName.'->'.$sAttCode] = $this->Get($sAttCode);
// #@# Note: This has been proven to be quite slow, this can slow down bulk load
$sAsHtml = $this->GetAsHtml($sAttCode);
$aScalarArgs[$sArgName.'->html('.$sAttCode.')'] = $sAsHtml;
$aScalarArgs[$sArgName.'->label('.$sAttCode.')'] = $this->GetEditValue($sAttCode); // "Nice" display value, but without HTML tags and entities
}
// Do something for case logs... quick N' dirty...
if ($aScalarArgs[$sArgName.'->'.$sAttCode] instanceof ormCaseLog)
elseif ($oAttDef->IsLinkSet())
{
$oCaseLog = $aScalarArgs[$sArgName.'->'.$sAttCode];
$aScalarArgs[$sArgName.'->'.$sAttCode] = $oCaseLog->GetText();
$aScalarArgs[$sArgName.'->head('.$sAttCode.')'] = $oCaseLog->GetLatestEntry();
$sRemoteName = $oAttDef->IsIndirect() ? $oAttDef->GetExtKeyToRemote().'_friendlyname' : 'friendlyname';
$oLinkSet = clone $this->Get($sAttCode); // Workaround/Safety net for Trac #887
$iLimit = MetaModel::GetConfig()->Get('max_linkset_output');
if ($iLimit > 0)
{
$oLinkSet->SetLimit($iLimit);
}
$aNames = $oLinkSet->GetColumnAsArray($sRemoteName);
if ($iLimit > 0)
{
$iTotal = $oLinkSet->Count();
if ($iTotal > count($aNames))
{
$aNames[] = '... '.Dict::Format('UI:TruncatedResults', count($aNames), $iTotal);
}
}
$sNames = implode("\n", $aNames);
$aScalarArgs[$sArgName.'->'.$sAttCode] = $sNames;
$aScalarArgs[$sArgName.'->html('.$sAttCode.')'] = '<ul><li>'.implode("</li><li>", $aNames).'</li></ul>';
}
}
@@ -2100,7 +2346,7 @@ abstract class DBObject
$iDepth = $bPropagate ? $iMaxDepth - 1 : 0;
$oFlt = DBObjectSearch::FromOQL($sQuery);
$oObjSet = new DBObjectSet($oFlt, array(), $this->ToArgs());
$oObjSet = new DBObjectSet($oFlt, array(), $this->ToArgsForQuery());
while ($oObj = $oObjSet->Fetch())
{
$sRootClass = MetaModel::GetRootClass(get_class($oObj));
@@ -2155,6 +2401,11 @@ abstract class DBObject
private function MakeDeletionPlan(&$oDeletionPlan, $aVisited = array(), $iDeleteOption = null)
{
static $iLoopTimeLimit = null;
if ($iLoopTimeLimit == null)
{
$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
}
$sClass = get_class($this);
$iThisId = $this->GetKey();
@@ -2179,10 +2430,17 @@ abstract class DBObject
$oDeletionPlan->SetDeletionIssues($this, $this->m_aDeleteIssues, $this->m_bSecurityIssue);
$aDependentObjects = $this->GetReferencingObjects(true /* allow all data */);
// Getting and setting time limit are not symetric:
// www.php.net/manual/fr/function.set-time-limit.php#72305
$iPreviousTimeLimit = ini_get('max_execution_time');
foreach ($aDependentObjects as $sRemoteClass => $aPotentialDeletes)
{
foreach ($aPotentialDeletes as $sRemoteExtKey => $aData)
{
set_time_limit($iLoopTimeLimit);
$oAttDef = $aData['attribute'];
$iDeletePropagationOption = $oAttDef->GetDeletionPropagationOption();
$oDepSet = $aData['objects'];
@@ -2212,6 +2470,7 @@ abstract class DBObject
}
}
}
set_time_limit($iPreviousTimeLimit);
}
/**
@@ -2266,9 +2525,6 @@ abstract class DBObject
public function InSyncScope()
{
return true;
// TODO - FINALIZE THIS OPTIMIZATION
//
// Optimization: cache the list of Data Sources and classes candidates for synchro
//
@@ -2281,12 +2537,50 @@ abstract class DBObject
while($oSource = $oSourceSet->Fetch())
{
$sTarget = $oSource->Get('scope_class');
$aSynchroClasses[] = $oSource;
$aSynchroClasses[] = $sTarget;
}
}
// to be continued...
foreach($aSynchroClasses as $sClass)
{
if ($this instanceof $sClass)
{
return true;
}
}
return false;
}
/////////////////////////////////////////////////////////////////////////
//
// Experimental iDisplay implementation
//
/////////////////////////////////////////////////////////////////////////
public static function MapContextParam($sContextParam)
{
return null;
}
public function GetHilightClass()
{
return HILIGHT_CLASS_NONE;
}
public function DisplayDetails(WebPage $oPage, $bEditMode = false)
{
$oPage->add('<h1>'.MetaModel::GetName(get_class($this)).': '.$this->GetName().'</h1>');
$aValues = array();
$aList = MetaModel::FlattenZList(MetaModel::GetZListItems(get_class($this), 'details'));
if (empty($aList))
{
$aList = array_keys(MetaModel::ListAttributeDefs(get_class($this)));
}
foreach($aList as $sAttCode)
{
$aValues[$sAttCode] = array('label' => MetaModel::GetLabel(get_class($this), $sAttCode), 'value' => $this->GetAsHTML($sAttCode));
}
$oPage->details($aValues);
}
}
?>

View File

@@ -72,11 +72,11 @@ class DBObjectSearch
}
/**
* Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects
* Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects)
**/
public function DeepClone()
{
return unserialize(serialize($this));
return unserialize(serialize($this)); // Beware this serializes/unserializes the search and its parameters as well
}
public function AllowAllData() {$this->m_bAllowAllData = true;}
@@ -118,6 +118,60 @@ class DBObjectSearch
return key($this->m_aClasses);
}
/**
* Change the class (only subclasses are supported as of now, because the conditions must fit the new class)
* Defaults to the first selected class (most of the time it is also the first joined class
*/
public function ChangeClass($sNewClass, $sAlias = null)
{
if (is_null($sAlias))
{
$sAlias = $this->GetClassAlias();
}
else
{
if (!array_key_exists($sAlias, $this->m_aClasses))
{
// discard silently - necessary when recursing on the related nodes (see code below)
return;
}
}
$sCurrClass = $this->GetClassName($sAlias);
if (!MetaModel::IsParentClass($sCurrClass, $sNewClass))
{
throw new Exception("Could not change the search class from '$sCurrClass' to '$sNewClass'. Only child classes are permitted.");
}
// Change for this node
//
$this->m_aSelectedClasses[$sAlias] = $sNewClass;
$this->m_aClasses[$sAlias] = $sNewClass;
// Change for all the related node (yes, this was necessary with some queries - strange effects otherwise)
//
foreach($this->m_aRelatedTo as $aRelatedTo)
{
$aRelatedTo['flt']->ChangeClass($sNewClass, $sAlias);
}
foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
{
foreach($aPointingTo as $iOperatorCode => $aFilter)
{
foreach($aFilter as $oExtFilter)
{
$oExtFilter->ChangeClass($sNewClass, $sAlias);
}
}
}
foreach($this->m_aReferencedBy as $sForeignClass => $aReferences)
{
foreach($aReferences as $sForeignExtKeyAttCode => $oForeignFilter)
{
$oForeignFilter->ChangeClass($sNewClass, $sAlias);
}
}
}
public function SetSelectedClasses($aNewSet)
{
$this->m_aSelectedClasses = array();
@@ -666,6 +720,19 @@ class DBObjectSearch
public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS)
{
if (!MetaModel::IsValidKeyAttCode($this->GetClass(), $sExtKeyAttCode))
{
throw new CoreWarning("The attribute code '$sExtKeyAttCode' is not an external key of the class '{$this->GetClass()}'");
}
$oAttExtKey = MetaModel::GetAttributeDef($this->GetClass(), $sExtKeyAttCode);
if(!MetaModel::IsSameFamilyBranch($oFilter->GetClass(), $oAttExtKey->GetTargetClass()))
{
throw new CoreException("The specified filter (pointing to {$oFilter->GetClass()}) is not compatible with the key '{$this->GetClass()}::$sExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}");
}
if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !($oAttExtKey instanceof AttributeHierarchicalKey))
{
throw new CoreException("The specified tree operator $iOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey");
}
// Note: though it seems to be a good practice to clone the given source filter
// (as it was done and fixed an issue in MergeWith())
// this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge)
@@ -680,20 +747,6 @@ class DBObjectSearch
protected function AddCondition_PointingTo_InNameSpace(DBObjectSearch $oFilter, $sExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode)
{
if (!MetaModel::IsValidKeyAttCode($this->GetClass(), $sExtKeyAttCode))
{
throw new CoreWarning("The attribute code '$sExtKeyAttCode' is not an external key of the class '{$this->GetClass()}' - the condition will be ignored");
}
$oAttExtKey = MetaModel::GetAttributeDef($this->GetClass(), $sExtKeyAttCode);
if(!MetaModel::IsSameFamilyBranch($oFilter->GetClass(), $oAttExtKey->GetTargetClass()))
{
throw new CoreException("The specified filter (pointing to {$oFilter->GetClass()}) is not compatible with the key '{$this->GetClass()}::$sExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}");
}
if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !($oAttExtKey instanceof AttributeHierarchicalKey))
{
throw new CoreException("The specified tree operator $iOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey");
}
// Find the node on which the new tree must be attached (most of the time it is "this")
$oReceivingFilter = $this->GetNode($this->GetClassAlias());
@@ -703,6 +756,17 @@ class DBObjectSearch
public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode)
{
$sForeignClass = $oFilter->GetClass();
if (!MetaModel::IsValidKeyAttCode($sForeignClass, $sForeignExtKeyAttCode))
{
throw new CoreException("The attribute code '$sForeignExtKeyAttCode' is not an external key of the class '{$sForeignClass}'");
}
$oAttExtKey = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode);
if(!MetaModel::IsSameFamilyBranch($this->GetClass(), $oAttExtKey->GetTargetClass()))
{
// à refaire en spécifique dans FromOQL
throw new CoreException("The specified filter (objects referencing an object of class {$this->GetClass()}) is not compatible with the key '{$sForeignClass}::$sForeignExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}");
}
// Note: though it seems to be a good practice to clone the given source filter
// (as it was done and fixed an issue in MergeWith())
// this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge)
@@ -718,16 +782,6 @@ class DBObjectSearch
protected function AddCondition_ReferencedBy_InNameSpace(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, &$aClassAliases, &$aAliasTranslation)
{
$sForeignClass = $oFilter->GetClass();
$sForeignClassAlias = $oFilter->GetClassAlias();
if (!MetaModel::IsValidKeyAttCode($sForeignClass, $sForeignExtKeyAttCode))
{
throw new CoreException("The attribute code '$sForeignExtKeyAttCode' is not an external key of the class '{$sForeignClass}' - the condition will be ignored");
}
$oAttExtKey = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode);
if(!MetaModel::IsSameFamilyBranch($this->GetClass(), $oAttExtKey->GetTargetClass()))
{
throw new CoreException("The specified filter (objects referencing an object of class {$this->GetClass()}) is not compatible with the key '{$sForeignClass}::$sForeignExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}");
}
// Find the node on which the new tree must be attached (most of the time it is "this")
$oReceivingFilter = $this->GetNode($this->GetClassAlias());
@@ -1075,7 +1129,7 @@ class DBObjectSearch
$sFltCode = $oExpression->GetName();
if (empty($sClassAlias))
{
// Try to find an alias
// Need to find the right alias
// Build an array of field => array of aliases
$aFieldClasses = array();
foreach($aClassAliases as $sAlias => $sReal)
@@ -1085,29 +1139,8 @@ class DBObjectSearch
$aFieldClasses[$sAnFltCode][] = $sAlias;
}
}
if (!array_key_exists($sFltCode, $aFieldClasses))
{
throw new OqlNormalizeException('Unknown filter code', $sQuery, $oExpression->GetNameDetails(), array_keys($aFieldClasses));
}
if (count($aFieldClasses[$sFltCode]) > 1)
{
throw new OqlNormalizeException('Ambiguous filter code', $sQuery, $oExpression->GetNameDetails());
}
$sClassAlias = $aFieldClasses[$sFltCode][0];
}
else
{
if (!array_key_exists($sClassAlias, $aClassAliases))
{
throw new OqlNormalizeException('Unknown class [alias]', $sQuery, $oExpression->GetParentDetails(), array_keys($aClassAliases));
}
$sClass = $aClassAliases[$sClassAlias];
if (!MetaModel::IsValidFilterCode($sClass, $sFltCode))
{
throw new OqlNormalizeException('Unknown filter code', $sQuery, $oExpression->GetNameDetails(), MetaModel::GetFiltersList($sClass));
}
}
return new FieldExpression($sFltCode, $sClassAlias);
}
elseif ($oExpression instanceof VariableOqlExpression)
@@ -1188,15 +1221,13 @@ class DBObjectSearch
$oOql = new OqlInterpreter($sQuery);
$oOqlQuery = $oOql->ParseObjectQuery();
$oMetaModel = new ModelReflectionRuntime();
$oOqlQuery->Check($oMetaModel, $sQuery); // Exceptions thrown in case of issue
$sClass = $oOqlQuery->GetClass();
$sClassAlias = $oOqlQuery->GetClassAlias();
if (!MetaModel::IsValidClass($sClass))
{
throw new UnknownClassOqlException($sQuery, $oOqlQuery->GetClassDetails(), MetaModel::GetClasses());
}
$oResultFilter = new DBObjectSearch($sClass, $sClassAlias);
$aAliases = array($sClassAlias => $sClass);
@@ -1212,21 +1243,6 @@ class DBObjectSearch
{
$sJoinClass = $oJoinSpec->GetClass();
$sJoinClassAlias = $oJoinSpec->GetClassAlias();
if (!MetaModel::IsValidClass($sJoinClass))
{
throw new UnknownClassOqlException($sQuery, $oJoinSpec->GetClassDetails(), MetaModel::GetClasses());
}
if (array_key_exists($sJoinClassAlias, $aAliases))
{
if ($sJoinClassAlias != $sJoinClass)
{
throw new OqlNormalizeException('Duplicate class alias', $sQuery, $oJoinSpec->GetClassAliasDetails());
}
else
{
throw new OqlNormalizeException('Duplicate class name', $sQuery, $oJoinSpec->GetClassDetails());
}
}
// Assumption: ext key on the left only !!!
// normalization should take care of this
@@ -1236,32 +1252,17 @@ class DBObjectSearch
$oRightField = $oJoinSpec->GetRightField();
$sToClass = $oRightField->GetParent();
$sPKeyDescriptor = $oRightField->GetName();
if ($sPKeyDescriptor != 'id')
{
throw new OqlNormalizeException('Wrong format for Join clause (right hand), expecting an id', $sQuery, $oRightField->GetNameDetails(), array('id'));
}
$aAliases[$sJoinClassAlias] = $sJoinClass;
$aJoinItems[$sJoinClassAlias] = new DBObjectSearch($sJoinClass, $sJoinClassAlias);
if (!array_key_exists($sFromClass, $aJoinItems))
{
throw new OqlNormalizeException('Unknown class in join condition (left expression)', $sQuery, $oLeftField->GetParentDetails(), array_keys($aJoinItems));
}
if (!array_key_exists($sToClass, $aJoinItems))
{
throw new OqlNormalizeException('Unknown class in join condition (right expression)', $sQuery, $oRightField->GetParentDetails(), array_keys($aJoinItems));
}
$aExtKeys = array_keys(MetaModel::GetExternalKeys($aAliases[$sFromClass]));
if (!in_array($sExtKeyAttCode, $aExtKeys))
{
throw new OqlNormalizeException('Unknown external key in join condition (left expression)', $sQuery, $oLeftField->GetNameDetails(), $aExtKeys);
}
if ($sFromClass == $sJoinClassAlias)
{
$aJoinItems[$sToClass]->AddCondition_ReferencedBy($aJoinItems[$sFromClass], $sExtKeyAttCode);
$oReceiver = $aJoinItems[$sToClass];
$oNewComer = $aJoinItems[$sFromClass];
$aAliasTranslation = array();
$oReceiver->AddCondition_ReferencedBy_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation);
}
else
{
@@ -1296,7 +1297,11 @@ class DBObjectSearch
$iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT;
break;
}
$aJoinItems[$sFromClass]->AddCondition_PointingTo($aJoinItems[$sToClass], $sExtKeyAttCode, $iOperatorCode);
$oReceiver = $aJoinItems[$sFromClass];
$oNewComer = $aJoinItems[$sToClass];
$aAliasTranslation = array();
$oReceiver->AddCondition_PointingTo_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation, $iOperatorCode);
}
}
}
@@ -1306,10 +1311,6 @@ class DBObjectSearch
foreach ($oOqlQuery->GetSelectedClasses() as $oClassDetails)
{
$sClassToSelect = $oClassDetails->GetValue();
if (!array_key_exists($sClassToSelect, $aAliases))
{
throw new OqlNormalizeException('Unknown class [alias]', $sQuery, $oClassDetails, array_keys($aAliases));
}
$aSelected[$sClassToSelect] = $aAliases[$sClassToSelect];
}
$oResultFilter->m_aClasses = $aAliases;

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2014 Combodo SARL
//
// This file is part of iTop.
//
@@ -26,20 +26,34 @@
/**
* A set of persistent objects, could be heterogeneous
* A set of persistent objects, could be heterogeneous as long as the objects in the set have a common ancestor class
*
* @package iTopORM
*/
class DBObjectSet
{
private $m_oFilter;
private $m_aAddedIds; // Ids of objects added (discrete lists)
private $m_aOrderBy;
protected $m_aAddedIds; // Ids of objects added (discrete lists)
protected $m_aAddedObjects;
protected $m_aArgs;
protected $m_aAttToLoad;
protected $m_aOrderBy;
public $m_bLoaded;
private $m_aData;
private $m_aId2Row;
private $m_iCurrRow;
protected $m_iNumTotalDBRows;
protected $m_iNumLoadedDBRows;
protected $m_iCurrRow;
protected $m_oFilter;
protected $m_oSQLResult;
/**
* Create a new set based on a Search definition.
*
* @param DBObjectSearch $oFilter The search filter defining the objects which are part of the set (multiple columns/objects per row are supported)
* @param hash $aOrderBy
* @param hash $aArgs Values to substitute for the search/query parameters (if any). Format: param_name => value
* @param hash $aExtendedDataSpec
* @param int $iLimitCount Maximum number of rows to load (i.e. equivalent to MySQL's LIMIT start, count)
* @param int $iLimitStart Index of the first row to load (i.e. equivalent to MySQL's LIMIT start, count)
*/
public function __construct(DBObjectSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0)
{
$this->m_oFilter = $oFilter->DeepClone();
@@ -51,15 +65,20 @@ class DBObjectSet
$this->m_iLimitCount = $iLimitCount;
$this->m_iLimitStart = $iLimitStart;
$this->m_iCount = null; // null if unknown yet
$this->m_iNumTotalDBRows = null; // Total number of rows for the query without LIMIT. null if unknown yet
$this->m_iNumLoadedDBRows = 0; // Total number of rows LOADED in $this->m_oSQLResult via a SQL query. 0 by default
$this->m_bLoaded = false; // true when the filter has been used OR the set is built step by step (AddObject...)
$this->m_aData = array(); // array of (row => array of (classalias) => object/null)
$this->m_aId2Row = array(); // array of (pkey => index in m_aData)
$this->m_aAddedObjects = array(); // array of (row => array of (classalias) => object/null) storing the objects added "in memory"
$this->m_iCurrRow = 0;
$this->m_oSQLResult = null;
}
public function __destruct()
{
if (is_object($this->m_oSQLResult))
{
$this->m_oSQLResult->free();
}
}
public function __toString()
@@ -82,6 +101,35 @@ class DBObjectSet
return $sRet;
}
public function __clone()
{
$this->m_oFilter = $this->m_oFilter->DeepClone();
$this->m_iNumTotalDBRows = null; // Total number of rows for the query without LIMIT. null if unknown yet
$this->m_iNumLoadedDBRows = 0; // Total number of rows LOADED in $this->m_oSQLResult via a SQL query. 0 by default
$this->m_bLoaded = false; // true when the filter has been used OR the set is built step by step (AddObject...)
$this->m_iCurrRow = 0;
$this->m_oSQLResult = null;
}
/**
* Called when unserializing a DBObjectSet
*/
public function __wakeup()
{
$this->m_iNumTotalDBRows = null; // Total number of rows for the query without LIMIT. null if unknown yet
$this->m_iNumLoadedDBRows = 0; // Total number of rows LOADED in $this->m_oSQLResult via a SQL query. 0 by default
$this->m_bLoaded = false; // true when the filter has been used OR the set is built step by step (AddObject...)
$this->m_iCurrRow = 0;
$this->m_oSQLResult = null;
}
/**
* Specify the subset of attributes to load (for each class of objects) before performing the SQL query for retrieving the rows from the DB
*
* @param hash $aAttToLoad Format: alias => array of attribute_codes
*
* @return void
*/
public function OptimizeColumnLoad($aAttToLoad)
{
if (is_null($aAttToLoad))
@@ -92,19 +140,22 @@ class DBObjectSet
{
// Complete the attribute list with the attribute codes
$aAttToLoadWithAttDef = array();
foreach($aAttToLoad as $sClassAlias => $aAttList)
foreach($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass)
{
$aSelectedClasses = $this->m_oFilter->GetSelectedClasses();
$sClass = $aSelectedClasses[$sClassAlias];
foreach($aAttList as $sAttToLoad)
$aAttToLoadWithAttDef[$sClassAlias] = array();
if (array_key_exists($sClassAlias, $aAttToLoad))
{
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad);
$aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad] = $oAttDef;
if ($oAttDef->IsExternalKey())
$aAttList = $aAttToLoad[$sClassAlias];
foreach($aAttList as $sAttToLoad)
{
// Add the external key friendly name anytime
$oFriendlyNameAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad.'_friendlyname');
$aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad.'_friendlyname'] = $oFriendlyNameAttDef;
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad);
$aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad] = $oAttDef;
if ($oAttDef->IsExternalKey())
{
// Add the external key friendly name anytime
$oFriendlyNameAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad.'_friendlyname');
$aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad.'_friendlyname'] = $oFriendlyNameAttDef;
}
}
}
// Add the friendly name anytime
@@ -112,7 +163,7 @@ class DBObjectSet
$aAttToLoadWithAttDef[$sClassAlias]['friendlyname'] = $oFriendlyNameAttDef;
// Make sure that the final class is requested anytime, whatever the specification (needed for object construction!)
if (!MetaModel::IsStandaloneClass($sClass) && !array_key_exists('finalclass', $aAttList))
if (!MetaModel::IsStandaloneClass($sClass) && !array_key_exists('finalclass', $aAttToLoadWithAttDef[$sClassAlias]))
{
$aAttToLoadWithAttDef[$sClassAlias]['finalclass'] = MetaModel::GetAttributeDef($sClass, 'finalclass');
}
@@ -122,6 +173,13 @@ class DBObjectSet
}
}
/**
* Create a set (in-memory) containing just the given object
*
* @param DBobject $oObject
*
* @return DBObjectSet The singleton set
*/
static public function FromObject($oObject)
{
$oRetSet = self::FromScratch(get_class($oObject));
@@ -129,17 +187,31 @@ class DBObjectSet
return $oRetSet;
}
/**
* Create an empty set (in-memory), for the given class (and its subclasses) of objects
*
* @param string $sClass The class (or an ancestor) for the objects to be added in this set
*
* @return DBObject The empty set
*/
static public function FromScratch($sClass)
{
$oFilter = new DBObjectSearch($sClass);
$oFilter->AddConditionExpression(new FalseExpression());
$oRetSet = new self($oFilter);
$oRetSet->m_bLoaded = true; // no DB load
$oRetSet->m_iNumTotalDBRows = 0; // Nothing from the DB
return $oRetSet;
}
// create an object set ex nihilo
// input = array of objects
/**
* Create a set (in-memory) with just one column (i.e. one object per row) and filled with the given array of objects
*
* @param string $sClass The class of the objects (must be a common ancestor to all objects in the set)
* @param array $aObjects The list of objects to add into the set
*
* @return DBObjectSet
*/
static public function FromArray($sClass, $aObjects)
{
$oRetSet = self::FromScratch($sClass);
@@ -147,21 +219,30 @@ class DBObjectSet
return $oRetSet;
}
// create an object set ex nihilo
// aClasses = array of (alias => class)
// input = array of (array of (classalias => object))
/**
* Create a set in-memory with several classes of objects per row (with one alias per "column")
*
* Limitation:
* The filter/OQL query representing such a set can not be rebuilt (only the first column will be taken into account)
*
* @param hash $aClasses Format: array of (alias => class)
* @param hash $aObjects Format: array of (array of (classalias => object))
*
* @return DBObjectSet
*/
static public function FromArrayAssoc($aClasses, $aObjects)
{
// In a perfect world, we should create a complete tree of DBObjectSearch,
// but as we lack most of the information related to the objects,
// let's create one search definition
// let's create one search definition corresponding only to the first column
$sClass = reset($aClasses);
$sAlias = key($aClasses);
$oFilter = new CMDBSearchFilter($sClass, $sAlias);
$oRetSet = new self($oFilter);
$oRetSet->m_bLoaded = true; // no DB load
$oRetSet->m_iNumTotalDBRows = 0; // Nothing from the DB
foreach($aObjects as $rowIndex => $aObjectsByClassAlias)
{
$oRetSet->AddObjectExtended($aObjectsByClassAlias);
@@ -206,11 +287,13 @@ class DBObjectSet
public function ToArrayOfValues()
{
if (!$this->m_bLoaded) $this->Load();
$this->Rewind();
$aSelectedClasses = $this->m_oFilter->GetSelectedClasses();
$aRet = array();
foreach($this->m_aData as $iRow => $aObjects)
$iRow = 0;
while($aObjects = $this->FetchAssoc())
{
foreach($aObjects as $sClassAlias => $oObject)
{
@@ -246,6 +329,7 @@ class DBObjectSet
}
}
}
$iRow++;
}
return $aRet;
}
@@ -268,11 +352,21 @@ class DBObjectSet
return $aRet;
}
/**
* Retrieve the DBObjectSearch corresponding to the objects present in this set
*
* Limitation:
* This method will NOT work for sets with several columns (i.e. several objects per row)
*
* @return DBObjectSearch
*/
public function GetFilter()
{
// Make sure that we carry on the parameters of the set with the filter
$oFilter = $this->m_oFilter->DeepClone();
$oFilter->SetInternalParams(array_merge($oFilter->GetInternalParams(), $this->m_aArgs));
// Note: the arguments found within a set can be object (but not in a filter)
// That's why PrepareQueryArguments must be invoked there
$oFilter->SetInternalParams(array_merge($oFilter->GetInternalParams(), MetaModel::PrepareQueryArguments($this->m_aArgs)));
if (count($this->m_aAddedIds) == 0)
{
@@ -288,37 +382,72 @@ class DBObjectSet
}
}
/**
* The (common ancestor) class of the objects in the first column of this set
*
* @return string The class of the objects in the first column
*/
public function GetClass()
{
return $this->m_oFilter->GetClass();
}
/**
* The alias for the class of the objects in the first column of this set
*
* @return string The alias of the class in the first column
*/
public function GetClassAlias()
{
return $this->m_oFilter->GetClassAlias();
}
/**
* The list of all classes (one per column) which are part of this set
*
* @return hash Format: alias => class
*/
public function GetSelectedClasses()
{
return $this->m_oFilter->GetSelectedClasses();
}
/**
* The root class (i.e. highest ancestor in the MeaModel class hierarchy) for the first column on this set
*
* @return string The root class for the objects in the first column of the set
*/
public function GetRootClass()
{
return MetaModel::GetRootClass($this->GetClass());
}
/**
* The arguments used for building this set
*
* @return hash Format: parameter_name => value
*/
public function GetArgs()
{
return $this->m_aArgs;
}
/**
* Sets the limits for loading the rows from the DB. Equivalent to MySQL's LIMIT start,count clause.
* @param int $iLimitCount The number of rows to load
* @param int $iLimitStart The index of the first row to load
*/
public function SetLimit($iLimitCount, $iLimitStart = 0)
{
$this->m_iLimitCount = $iLimitCount;
$this->m_iLimitStart = $iLimitStart;
}
/**
* Sets the sort order for loading the rows from the DB. Changing the order by causes a Reload.
*
* @param hash $aOrderBy Format: field_code => boolean (true = ascending, false = descending)
*/
public function SetOrderBy($aOrderBy)
{
if ($this->m_aOrderBy != $aOrderBy)
@@ -332,16 +461,33 @@ class DBObjectSet
}
}
/**
* Returns the 'count' limit for loading the rows from the DB
*
* @return int
*/
public function GetLimitCount()
{
return $this->m_iLimitCount;
}
/**
* Returns the 'start' limit for loading the rows from the DB
*
* @return int
*/
public function GetLimitStart()
{
return $this->m_iLimitStart;
}
/**
* Get the sort order used for loading this set from the database
*
* Limitation: the sort order has no effect on objects added in-memory
*
* @return hash Format: field_code => boolean (true = ascending, false = descending)
*/
public function GetRealSortOrder()
{
// Get the class default sort order if not specified with the API
@@ -356,6 +502,9 @@ class DBObjectSet
}
}
/**
* Loads the set from the database. Actually performs the SQL query to retrieve the records from the DB.
*/
public function Load()
{
if ($this->m_bLoaded) return;
@@ -370,87 +519,150 @@ class DBObjectSet
{
$sSQL = MetaModel::MakeSelectQuery($this->m_oFilter, $this->GetRealSortOrder(), $this->m_aArgs, $this->m_aAttToLoad, $this->m_aExtendedDataSpec);
}
$resQuery = CMDBSource::Query($sSQL);
if (!$resQuery) return;
$sClass = $this->m_oFilter->GetClass();
while ($aRow = CMDBSource::FetchArray($resQuery))
if (is_object($this->m_oSQLResult))
{
$aObjects = array();
foreach ($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass)
{
if (is_null($aRow[$sClassAlias.'id']))
{
$oObject = null;
}
else
{
$oObject = MetaModel::GetObjectByRow($sClass, $aRow, $sClassAlias, $this->m_aAttToLoad, $this->m_aExtendedDataSpec);
}
$aObjects[$sClassAlias] = $oObject;
}
$this->AddObjectExtended($aObjects, true /* internal load */);
// Free previous resultset if any
$this->m_oSQLResult->free();
$this->m_oSQLResult = null;
}
CMDBSource::FreeResult($resQuery);
$this->m_iNumTotalDBRows = null;
$this->m_oSQLResult = CMDBSource::Query($sSQL);
if ($this->m_oSQLResult === false) return;
if (($this->m_iLimitCount == 0) && ($this->m_iLimitStart == 0))
{
$this->m_iNumTotalDBRows = $this->m_oSQLResult->num_rows;
}
$this->m_iNumLoadedDBRows = $this->m_oSQLResult->num_rows;
}
/**
* The total number of rows in this set. Independently of the SetLimit used for loading the set and taking into account the rows added in-memory.
*
* May actually perform the SQL query SELECT COUNT... if the set was not previously loaded, or loaded with a SetLimit
*
* @return int The total number of rows for this set.
*/
public function Count()
{
if ($this->m_bLoaded && ($this->m_iLimitCount == 0) && ($this->m_iLimitStart == 0))
if (is_null($this->m_iNumTotalDBRows))
{
return count($this->m_aData);
}
else
{
if (is_null($this->m_iCount))
{
$sSQL = MetaModel::MakeSelectQuery($this->m_oFilter, array(), $this->m_aArgs, null, null, 0, 0, true);
$resQuery = CMDBSource::Query($sSQL);
if (!$resQuery) return 0;
$aRow = CMDBSource::FetchArray($resQuery);
CMDBSource::FreeResult($resQuery);
$this->m_iCount = $aRow['COUNT'];
}
return $this->m_iCount;
$sSQL = MetaModel::MakeSelectQuery($this->m_oFilter, array(), $this->m_aArgs, null, null, 0, 0, true);
$resQuery = CMDBSource::Query($sSQL);
if (!$resQuery) return 0;
$aRow = CMDBSource::FetchArray($resQuery);
CMDBSource::FreeResult($resQuery);
$this->m_iNumTotalDBRows = $aRow['COUNT'];
}
return $this->m_iNumTotalDBRows + count($this->m_aAddedObjects); // Does it fix Trac #887 ??
}
/**
* Number of rows available in memory (loaded from DB + added in memory)
*
* @return number The number of rows available for Fetch'ing
*/
protected function CountLoaded()
{
return $this->m_iNumLoadedDBRows + count($this->m_aAddedObjects);
}
public function Fetch($sClassAlias = '')
/**
* Fetch the object (with the given class alias) at the current position in the set and move the cursor to the next position.
*
* @param string $sRequestedClassAlias The class alias to fetch (if there are several objects/classes per row)
* @return DBObject The fetched object or null when at the end
*/
public function Fetch($sRequestedClassAlias = '')
{
if (!$this->m_bLoaded) $this->Load();
if ($this->m_iCurrRow >= count($this->m_aData))
if ($this->m_iCurrRow >= $this->CountLoaded())
{
return null;
}
if (strlen($sClassAlias) == 0)
if (strlen($sRequestedClassAlias) == 0)
{
$sClassAlias = $this->m_oFilter->GetClassAlias();
$sRequestedClassAlias = $this->m_oFilter->GetClassAlias();
}
if ($this->m_iCurrRow < $this->m_iNumLoadedDBRows)
{
// Pick the row from the database
$aRow = CMDBSource::FetchArray($this->m_oSQLResult);
foreach ($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass)
{
if ($sRequestedClassAlias == $sClassAlias)
{
if (is_null($aRow[$sClassAlias.'id']))
{
$oRetObj = null;
}
else
{
$oRetObj = MetaModel::GetObjectByRow($sClass, $aRow, $sClassAlias, $this->m_aAttToLoad, $this->m_aExtendedDataSpec);
}
break;
}
}
}
else
{
// Pick the row from the objects added *in memory*
$oRetObj = $this->m_aAddedObjects[$this->m_iCurrRow - $this->m_iNumLoadedDBRows][$sRequestedClassAlias];
}
$oRetObj = $this->m_aData[$this->m_iCurrRow][$sClassAlias];
$this->m_iCurrRow++;
return $oRetObj;
}
// Return the whole line if several classes have been specified in the query
//
/**
* Fetch the whole row of objects (if several classes have been specified in the query) and move the cursor to the next position
*
* @return hash A hash with the format 'classAlias' => $oObj representing the current row of the set. Returns null when at the end.
*/
public function FetchAssoc()
{
if (!$this->m_bLoaded) $this->Load();
if ($this->m_iCurrRow >= count($this->m_aData))
if ($this->m_iCurrRow >= $this->CountLoaded())
{
return null;
}
$aRetObjects = $this->m_aData[$this->m_iCurrRow];
if ($this->m_iCurrRow < $this->m_iNumLoadedDBRows)
{
// Pick the row from the database
$aRow = CMDBSource::FetchArray($this->m_oSQLResult);
$aRetObjects = array();
foreach ($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass)
{
if (is_null($aRow[$sClassAlias.'id']))
{
$oObj = null;
}
else
{
$oObj = MetaModel::GetObjectByRow($sClass, $aRow, $sClassAlias, $this->m_aAttToLoad, $this->m_aExtendedDataSpec);
}
$aRetObjects[$sClassAlias] = $oObj;
}
}
else
{
// Pick the row from the objects added *in memory*
$oRetObj = $this->m_aAddedObjects[$this->m_iCurrRow - $this->m_iNumLoadedDBRows][$sRequestedClassAlias];
}
$this->m_iCurrRow++;
return $aRetObjects;
}
/**
* Position the cursor (for iterating in the set) to the first position (equivalent to Seek(0))
*/
public function Rewind()
{
if ($this->m_bLoaded)
@@ -459,14 +671,32 @@ class DBObjectSet
}
}
/**
* Position the cursor (for iterating in the set) to the given position
*
* @param int $iRow
*/
public function Seek($iRow)
{
if (!$this->m_bLoaded) $this->Load();
$this->m_iCurrRow = min($iRow, count($this->m_aData));
$this->m_iCurrRow = min($iRow, $this->Count());
if ($this->m_iCurrRow < $this->m_iNumLoadedDBRows)
{
$this->m_oSQLResult->data_seek($this->m_iCurrRow);
}
return $this->m_iCurrRow;
}
/**
* Add an object to the current set (in-memory only, nothing is written to the database)
*
* Limitation:
* Sets with several objects per row are NOT supported
*
* @param DBObject $oObject The object to add
* @param string $sClassAlias The alias for the class of the object
*/
public function AddObject($oObject, $sClassAlias = '')
{
if (!$this->m_bLoaded) $this->Load();
@@ -476,35 +706,52 @@ class DBObjectSet
$sClassAlias = $this->m_oFilter->GetClassAlias();
}
$iNextPos = count($this->m_aData);
$this->m_aData[$iNextPos][$sClassAlias] = $oObject;
$iNextPos = count($this->m_aAddedObjects);
$this->m_aAddedObjects[$iNextPos][$sClassAlias] = $oObject;
if (!is_null($oObject))
{
$this->m_aId2Row[$sClassAlias][$oObject->GetKey()] = $iNextPos;
$this->m_aAddedIds[$oObject->GetKey()] = true;
}
}
protected function AddObjectExtended($aObjectArray, $bInternalLoad = false)
/**
* Add a hash containig objects into the current set.
*
* The expected format for the hash is: $aObjectArray[$idx][$sClassAlias] => $oObject
* Limitation:
* The aliases MUST match the ones used in the current set
* Only the ID of the objects associated to the first alias (column) is remembered.. in case we have to rebuild a filter
*
* @param hash $aObjectArray
*/
protected function AddObjectExtended($aObjectArray)
{
if (!$this->m_bLoaded) $this->Load();
$iNextPos = count($this->m_aData);
$iNextPos = count($this->m_aAddedObjects);
$sFirstAlias = $this->m_oFilter->GetClassAlias();
foreach ($aObjectArray as $sClassAlias => $oObject)
{
$this->m_aData[$iNextPos][$sClassAlias] = $oObject;
if (!is_null($oObject))
$this->m_aAddedObjects[$iNextPos][$sClassAlias] = $oObject;
if (!is_null($oObject) && ($sFirstAlias == $sClassAlias))
{
$this->m_aId2Row[$sClassAlias][$oObject->GetKey()] = $iNextPos;
if (!$bInternalLoad)
{
$this->m_aAddedIds[$oObject->GetKey()] = true;
}
$this->m_aAddedIds[$oObject->GetKey()] = true;
}
}
}
/**
* Add an array of objects into the current set
*
* Limitation:
* Sets with several classes per row are not supported (use AddObjectExtended instead)
*
* @param array $aObjects The array of objects to add
* @param string $sClassAlias The Alias of the class for the added objects
*/
public function AddObjectArray($aObjects, $sClassAlias = '')
{
if (!$this->m_bLoaded) $this->Load();
@@ -516,7 +763,16 @@ class DBObjectSet
}
}
public function Merge($oObjectSet)
/**
* Append a given set to the current object. (This method used to be named Merge)
*
* Limitation:
* The added objects are not checked for duplicates (i.e. one cann add several times the same object, or add an object already present in the set).
*
* @param DBObjectSet $oObjectSet The set to append
* @throws CoreException
*/
public function Append(DBObjectSet $oObjectSet)
{
if ($this->GetRootClass() != $oObjectSet->GetRootClass())
{
@@ -531,7 +787,18 @@ class DBObjectSet
}
}
public function CreateIntersect($oObjectSet)
/**
* Create a set containing the objects present in both the current set and another specified set
*
* Limitations:
* Will NOT work if only a subset of the sets was loaded with SetLimit.
* Works only with sets made of objects loaded from the database since the comparison is based on the objects identifiers
*
* @param DBObjectSet $oObjectSet The set to intersect with. The current position inside the set will be lost (= at the end)
* @throws CoreException
* @return DBObjectSet A new set of objects, containing the objects present in both sets (based on their identifier)
*/
public function CreateIntersect(DBObjectSet $oObjectSet)
{
if ($this->GetRootClass() != $oObjectSet->GetRootClass())
{
@@ -539,58 +806,132 @@ class DBObjectSet
}
if (!$this->m_bLoaded) $this->Load();
$aId2Row = array();
$iCurrPos = $this->m_iCurrRow; // Save the cursor
$idx = 0;
while($oObj = $this->Fetch())
{
$aId2Row[$oObj->GetKey()] = $idx;
$idx++;
}
$oNewSet = DBObjectSet::FromScratch($this->GetClass());
$sClassAlias = $this->m_oFilter->GetClassAlias();
$oObjectSet->Seek(0);
while ($oObject = $oObjectSet->Fetch())
{
if (array_key_exists($oObject->GetKey(), $this->m_aId2Row[$sClassAlias]))
if (array_key_exists($oObject->GetKey(), $aId2Row))
{
$oNewSet->AddObject($oObject);
}
}
$this->Seek($iCurrPos); // Restore the cursor
return $oNewSet;
}
// Note: This verb works only with objects existing in the database
//
public function HasSameContents($oObjectSet)
/**
* Compare two sets of objects to determine if their content is identical or not.
*
* Limitation:
* Works only on objects written to the DB, since we rely on their identifiers
*
* @param DBObjectSet $oObjectSet
* @return boolean True if the sets are identical, false otherwise
*/
public function HasSameContents(DBObjectSet $oObjectSet)
{
if ($this->GetRootClass() != $oObjectSet->GetRootClass())
{
return false;
}
if (!$this->m_bLoaded) $this->Load();
if ($this->Count() != $oObjectSet->Count())
{
return false;
}
$sClassAlias = $this->m_oFilter->GetClassAlias();
$aId2Row = array();
$bRet = true;
$iCurrPos = $this->m_iCurrRow; // Save the cursor
$idx = 0;
// Optimization: we retain the first $iMaxObjects objects in memory
// to speed up the comparison of small sets (see below: $oObject->Equals($oSibling))
$iMaxObjects = 20;
$aCachedObjects = array();
while($oObj = $this->Fetch())
{
$aId2Row[$oObj->GetKey()] = $idx;
if ($idx <= $iMaxObjects)
{
$aCachedObjects[$idx] = $oObj;
}
$idx++;
}
$oObjectSet->Rewind();
while ($oObject = $oObjectSet->Fetch())
{
$iObjectKey = $oObject->GetKey();
if ($iObjectKey < 0)
{
return false;
$bRet = false;
break;
}
if (!array_key_exists($iObjectKey, $this->m_aId2Row[$sClassAlias]))
if (!array_key_exists($iObjectKey, $aId2Row))
{
return false;
$bRet = false;
break;
}
$iRow = $aId2Row[$iObjectKey];
if (array_key_exists($iRow, $aCachedObjects))
{
// Cache hit
$oSibling = $aCachedObjects[$iRow];
}
else
{
// Go fetch it from the DB, unless it's an object added in-memory
$oSibling = $this->GetObjectAt($iRow);
}
$iRow = $this->m_aId2Row[$sClassAlias][$iObjectKey];
$oSibling = $this->m_aData[$iRow][$sClassAlias];
if (!$oObject->Equals($oSibling))
{
return false;
$bRet = false;
break;
}
}
return true;
$this->Seek($iCurrPos); // Restore the cursor
return $bRet;
}
public function CreateDelta($oObjectSet)
protected function GetObjectAt($iIndex)
{
if (!$this->m_bLoaded) $this->Load();
// Save the current position for iteration
$iCurrPos = $this->m_iCurrRow;
$this->Seek($iIndex);
$oObject = $this->Fetch();
// Restore the current position for iteration
$this->Seek($this->m_iCurrRow);
return $oObject;
}
/**
* Build a new set (in memory) made of objects of the given set which are NOT present in the current set
*
* Limitations:
* The objects inside the set must be written in the database since the comparison is based on their identifiers
* Sets with several objects per row are NOT supported
*
* @param DBObjectSet $oObjectSet
* @throws CoreException
*
* @return DBObjectSet The "delta" set.
*/
public function CreateDelta(DBObjectSet $oObjectSet)
{
if ($this->GetRootClass() != $oObjectSet->GetRootClass())
{
@@ -598,20 +939,37 @@ class DBObjectSet
}
if (!$this->m_bLoaded) $this->Load();
$aId2Row = array();
$iCurrPos = $this->m_iCurrRow; // Save the cursor
$idx = 0;
while($oObj = $this->Fetch())
{
$aId2Row[$oObj->GetKey()] = $idx;
$idx++;
}
$oNewSet = DBObjectSet::FromScratch($this->GetClass());
$sClassAlias = $this->m_oFilter->GetClassAlias();
$oObjectSet->Seek(0);
while ($oObject = $oObjectSet->Fetch())
{
if (!array_key_exists($oObject->GetKey(), $this->m_aId2Row[$sClassAlias]))
if (!array_key_exists($oObject->GetKey(), $aId2Row))
{
$oNewSet->AddObject($oObject);
}
}
$this->Seek($iCurrPos); // Restore the cursor
return $oNewSet;
}
/**
* Compute the "RelatedObjects" (for the given relation, as defined by MetaModel::GetRelatedObjects) for a whole set of DBObjects
*
* @param string $sRelCode The code of the relation to use for the computation
* @param int $iMaxDepth Teh maximum recursion depth
*
* @return Array An array containg all the "related" objects
*/
public function GetRelatedObjects($sRelCode, $iMaxDepth = 99)
{
$aRelatedObjs = array();
@@ -640,7 +998,7 @@ class DBObjectSet
* in the set. If for a given attribute, objects in the set have various values
* then the resulting object will contain null for this value.
* @param $aValues Hash Output: the distribution of the values, in the set, for each attribute
* @return Object
* @return DBObject The object with the common values
*/
public function ComputeCommonObject(&$aValues)
{
@@ -736,14 +1094,8 @@ class DBObjectSet
{
foreach($aVals as $sCode => $oExpr)
{
if ($oExpr instanceof ScalarExpression)
{
$aConst[$sClassAlias][$sCode] = $oExpr->GetValue();
}
else //Variable
{
$aConst[$sClassAlias][$sCode] = $aScalarArgs[$oExpr->GetName()];
}
$oScalarExpr = $oExpr->GetAsScalar($aScalarArgs);
$aConst[$sClassAlias][$sCode] = $oScalarExpr->GetValue();
}
}
return $aConst;
@@ -756,11 +1108,23 @@ class DBObjectSet
{
if (MetaModel::IsValidObject($value))
{
$aScalarArgs = array_merge($aScalarArgs, $value->ToArgs($sArgName));
if (strpos($sArgName, '->object()') === false)
{
// Lazy syntax - develop the object contextual parameters
$aScalarArgs = array_merge($aScalarArgs, $value->ToArgsForQuery($sArgName));
}
else
{
// Leave as is
$aScalarArgs[$sArgName] = $value;
}
}
else
{
$aScalarArgs[$sArgName] = (string) $value;
if (!is_array($value)) // Sometimes ExtraParams contains a mix (like defaults[]) so non scalar parameters are ignored
{
$aScalarArgs[$sArgName] = (string) $value;
}
}
}
$aScalarArgs['current_contact_id'] = UserRights::GetContactId();

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -20,7 +20,7 @@
/**
* Algorithm to delete object(s) and maintain data integrity
*
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @copyright Copyright (C) 2010-2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -82,6 +82,9 @@ class DeletionPlan
public function ComputeResults()
{
$this->m_iToDelete = 0;
$this->m_iToUpdate = 0;
foreach($this->m_aToDelete as $sClass => $aToDelete)
{
foreach($aToDelete as $iId => $aData)
@@ -104,10 +107,15 @@ class DeletionPlan
}
}
// Getting and setting time limit are not symetric:
// www.php.net/manual/fr/function.set-time-limit.php#72305
$iPreviousTimeLimit = ini_get('max_execution_time');
$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
foreach($this->m_aToUpdate as $sClass => $aToUpdate)
{
foreach($aToUpdate as $iId => $aData)
{
set_time_limit($iLoopTimeLimit);
$this->m_iToUpdate++;
$oObject = $aData['to_reset'];
@@ -131,9 +139,9 @@ class DeletionPlan
$this->m_bFoundSecurityIssue = true;
}
}
}
}
set_time_limit($iPreviousTimeLimit);
}
public function GetIssues()

View File

@@ -120,7 +120,7 @@ class Dict
}
public static function S($sStringCode, $sDefault = null)
public static function S($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
{
// Attempt to find the string in the user language
//
@@ -134,19 +134,22 @@ class Dict
{
return $aCurrentDictionary[$sStringCode];
}
// Attempt to find the string in the default language
//
$aDefaultDictionary = self::$m_aData[self::$m_sDefaultLanguage];
if (array_key_exists($sStringCode, $aDefaultDictionary))
if (!$bUserLanguageOnly)
{
return $aDefaultDictionary[$sStringCode];
}
// Attempt to find the string in english
//
$aDefaultDictionary = self::$m_aData['EN US'];
if (array_key_exists($sStringCode, $aDefaultDictionary))
{
return $aDefaultDictionary[$sStringCode];
// Attempt to find the string in the default language
//
$aDefaultDictionary = self::$m_aData[self::$m_sDefaultLanguage];
if (array_key_exists($sStringCode, $aDefaultDictionary))
{
return $aDefaultDictionary[$sStringCode];
}
// Attempt to find the string in english
//
$aDefaultDictionary = self::$m_aData['EN US'];
if (array_key_exists($sStringCode, $aDefaultDictionary))
{
return $aDefaultDictionary[$sStringCode];
}
}
// Could not find the string...
//

View File

@@ -33,10 +33,15 @@ define ('EMAIL_SEND_OK', 0);
define ('EMAIL_SEND_PENDING', 1);
define ('EMAIL_SEND_ERROR', 2);
class EMail
{
// Serialization formats
const ORIGINAL_FORMAT = 1; // Original format, consisting in serializing the whole object, inculding the Swift Mailer's object.
// Did not work with attachements since their binary representation cannot be stored as a valid UTF-8 string
const FORMAT_V2 = 2; // New format, only the raw data are serialized (base64 encoded if needed)
protected static $m_oConfig = null;
protected $m_aData; // For storing data to serialize
public function LoadConfig($sConfigFile = ITOP_DEFAULT_CONFIG_FILE)
{
@@ -46,17 +51,95 @@ class EMail
}
}
protected $m_oMessage;
public function __construct()
{
$this->m_aData = array();
$this->m_oMessage = Swift_Message::newInstance();
$oEncoder = new Swift_Mime_ContentEncoder_PlainContentEncoder('8bit');
$this->m_oMessage->setEncoder($oEncoder);
}
/**
* Custom serialization method
* No longer use the brute force "serialize" method since
* 1) It does not work with binary attachments (since they cannot be stored in a UTF-8 text field)
* 2) The size tends to be quite big (sometimes ten times the size of the email)
*/
public function SerializeV2()
{
return serialize($this->m_aData);
}
/**
* Custom de-serialization method
* @param string $sSerializedMessage The serialized representation of the message
*/
static public function UnSerializeV2($sSerializedMessage)
{
$aData = unserialize($sSerializedMessage);
$oMessage = new Email();
if (array_key_exists('body', $aData))
{
$oMessage->SetBody($aData['body']['body'], $aData['body']['mimeType']);
}
if (array_key_exists('message_id', $aData))
{
$oMessage->SetMessageId($aData['message_id']);
}
if (array_key_exists('bcc', $aData))
{
$oMessage->SetRecipientBCC($aData['bcc']);
}
if (array_key_exists('cc', $aData))
{
$oMessage->SetRecipientCC($aData['cc']);
}
if (array_key_exists('from', $aData))
{
$oMessage->SetRecipientFrom($aData['from']['address'], $aData['from']['label']);
}
if (array_key_exists('reply_to', $aData))
{
$oMessage->SetRecipientReplyTo($aData['reply_to']);
}
if (array_key_exists('to', $aData))
{
$oMessage->SetRecipientTO($aData['to']);
}
if (array_key_exists('subject', $aData))
{
$oMessage->SetSubject($aData['subject']);
}
if (array_key_exists('headers', $aData))
{
foreach($aData['headers'] as $sKey => $sValue)
{
$oMessage->AddToHeader($sKey, $sValue);
}
}
if (array_key_exists('parts', $aData))
{
foreach($aData['parts'] as $aPart)
{
$oMessage->AddPart($aPart['text'], $aPart['mimeType']);
}
}
if (array_key_exists('attachments', $aData))
{
foreach($aData['attachments'] as $aAttachment)
{
$oMessage->AddAttachment(base64_decode($aAttachment['data']), $aAttachment['filename'], $aAttachment['mimeType']);
}
}
return $oMessage;
}
protected function SendAsynchronous(&$aIssues, $oLog = null)
{
try
@@ -101,10 +184,13 @@ class EMail
$oMailer = Swift_Mailer::newInstance($oTransport);
$iSent = $oMailer->send($this->m_oMessage);
if ($iSent === false)
$aFailedRecipients = array();
$iSent = $oMailer->send($this->m_oMessage, $aFailedRecipients);
if ($iSent === 0)
{
$aIssues = 'une erreur s\'est produite... mais quoi !!!';
// Beware: it seems that $aFailedRecipients sometimes contains the recipients that actually received the message !!!
IssueLog::Warning('Email sending failed: Some recipients were invalid, aFailedRecipients contains: '.implode(', ', $aFailedRecipients));
$aIssues = array('Some recipients were invalid.');
return EMAIL_SEND_ERROR;
}
else
@@ -136,6 +222,12 @@ class EMail
public function AddToHeader($sKey, $sValue)
{
if (!array_key_exists('headers', $this->m_aData))
{
$this->m_aData['headers'] = array();
}
$this->m_aData['headers'][$sKey] = $sValue;
if (strlen($sValue) > 0)
{
$oHeaders = $this->m_oMessage->getHeaders();
@@ -149,6 +241,8 @@ class EMail
public function SetMessageId($sId)
{
$this->m_aData['message_id'] = $sId;
// Note: Swift will add the angle brackets for you
// so let's remove the angle brackets if present, for historical reasons
$sId = str_replace(array('<', '>'), '', $sId);
@@ -164,22 +258,34 @@ class EMail
public function SetBody($sBody, $sMimeType = 'text/html')
{
$this->m_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType);
$this->m_oMessage->setBody($sBody, $sMimeType);
}
public function AddPart($sText, $sMimeType = 'text/html')
{
if (!array_key_exists('parts', $this->m_aData))
{
$this->m_aData['parts'] = array();
}
$this->m_aData['parts'][] = array('text' => $sText, 'mimeType' => $sMimeType);
$this->m_oMessage->addPart($sText, $sMimeType);
}
public function AddAttachment($data, $sFileName, $sMimeType)
{
if (!array_key_exists('attachments', $this->m_aData))
{
$this->m_aData['attachments'] = array();
}
$this->m_aData['attachments'][] = array('data' => base64_encode($data), 'filename' => $sFileName, 'mimeType' => $sMimeType);
$this->m_oMessage->attach(Swift_Attachment::newInstance($data, $sFileName, $sMimeType));
}
public function SetSubject($aSubject)
public function SetSubject($sSubject)
{
$this->m_oMessage->setSubject($aSubject);
$this->m_aData['subject'] = $sSubject;
$this->m_oMessage->setSubject($sSubject);
}
public function GetSubject()
@@ -187,11 +293,30 @@ class EMail
return $this->m_oMessage->getSubject();
}
/**
* Helper to transform and sanitize addresses
* - get rid of empty addresses
*/
protected function AddressStringToArray($sAddressCSVList)
{
$aAddresses = array();
foreach(explode(',', $sAddressCSVList) as $sAddress)
{
$sAddress = trim($sAddress);
if (strlen($sAddress) > 0)
{
$aAddresses[] = $sAddress;
}
}
return $aAddresses;
}
public function SetRecipientTO($sAddress)
{
$this->m_aData['to'] = $sAddress;
if (!empty($sAddress))
{
$aAddresses = explode(', ', $sAddress);
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setTo($aAddresses);
}
}
@@ -199,6 +324,11 @@ class EMail
public function GetRecipientTO($bAsString = false)
{
$aRes = $this->m_oMessage->getTo();
if ($aRes === null)
{
// There is no "To" header field
$aRes = array();
}
if ($bAsString)
{
$aStrings = array();
@@ -224,24 +354,27 @@ class EMail
public function SetRecipientCC($sAddress)
{
$this->m_aData['cc'] = $sAddress;
if (!empty($sAddress))
{
$aAddresses = explode(', ', $sAddress);
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setCc($aAddresses);
}
}
public function SetRecipientBCC($sAddress)
{
$this->m_aData['bcc'] = $sAddress;
if (!empty($sAddress))
{
$aAddresses = explode(', ', $sAddress);
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setBcc($aAddresses);
}
}
public function SetRecipientFrom($sAddress, $sLabel = '')
{
$this->m_aData['from'] = array('address' => $sAddress, 'label' => $sLabel);
if ($sLabel != '')
{
$this->m_oMessage->setFrom(array($sAddress => $sLabel));
@@ -254,6 +387,7 @@ class EMail
public function SetRecipientReplyTo($sAddress)
{
$this->m_aData['reply_to'] = $sAddress;
if (!empty($sAddress))
{
$this->m_oMessage->setReplyTo($sAddress);

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -41,6 +41,7 @@ class Event extends DBObject implements iDisplay
"db_key_field" => "id",
"db_finalclass_field" => "realclass",
"display_template" => "",
"order_by_default" => array('date' => false)
);
MetaModel::Init_Params($aParams);
//MetaModel::Init_InheritAttributes();
@@ -106,7 +107,7 @@ class Event extends DBObject implements iDisplay
function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array())
{
if ($bEditMode) return; // Not editable
if ($bEditMode) return array(); // Not editable
$aDetails = array();
$sClass = get_class($this);
@@ -117,6 +118,7 @@ class Event extends DBObject implements iDisplay
$aDetails[] = array('label' => '<span title="'.MetaModel::GetDescription($sClass, $sAttCode).'">'.MetaModel::GetLabel($sClass, $sAttCode).'</span>', 'value' => $sDisplayValue);
}
$oPage->Details($aDetails);
return array();
}
}
@@ -135,6 +137,10 @@ class EventNotification extends Event
"db_key_field" => "id",
"db_finalclass_field" => "",
"display_template" => "",
"order_by_default" => array('date' => false),
'indexes' => array(
array('object_id'),
)
);
MetaModel::Init_Params($aParams);
MetaModel::Init_InheritAttributes();
@@ -167,6 +173,7 @@ class EventNotificationEmail extends EventNotification
"db_key_field" => "id",
"db_finalclass_field" => "",
"display_template" => "",
"order_by_default" => array('date' => false)
);
MetaModel::Init_Params($aParams);
MetaModel::Init_InheritAttributes();
@@ -204,6 +211,7 @@ class EventIssue extends Event
"db_key_field" => "id",
"db_finalclass_field" => "",
"display_template" => "",
"order_by_default" => array('date' => false)
);
MetaModel::Init_Params($aParams);
MetaModel::Init_InheritAttributes();
@@ -303,6 +311,7 @@ class EventWebService extends Event
"db_key_field" => "id",
"db_finalclass_field" => "",
"display_template" => "",
"order_by_default" => array('date' => false)
);
MetaModel::Init_Params($aParams);
MetaModel::Init_InheritAttributes();
@@ -337,6 +346,7 @@ class EventLoginUsage extends Event
"db_table" => "priv_event_loginusage",
"db_key_field" => "id",
"db_finalclass_field" => "",
"order_by_default" => array('date' => false)
);
MetaModel::Init_Params($aParams);
MetaModel::Init_InheritAttributes();

View File

@@ -383,7 +383,7 @@ class ScalarExpression extends UnaryExpression
{
public function __construct($value)
{
if (!is_scalar($value) && !is_null($value))
if (!is_scalar($value) && !is_null($value) && (!$value instanceof OqlHexValue))
{
throw new CoreException('Attempt to create a scalar expression from a non scalar', array('var_type'=>gettype($value)));
}
@@ -403,6 +403,11 @@ class ScalarExpression extends UnaryExpression
}
return $sRet;
}
public function GetAsScalar($aArgs)
{
return clone $this;
}
}
class TrueExpression extends ScalarExpression
@@ -611,14 +616,29 @@ class VariableExpression extends UnaryExpression
{
return CMDBSource::Quote($aArgs[$this->m_sName]);
}
elseif ($bRetrofitParams)
elseif (($iPos = strpos($this->m_sName, '->')) !== false)
{
$sParamName = substr($this->m_sName, 0, $iPos);
if (array_key_exists($sParamName.'->object()', $aArgs))
{
$sAttCode = substr($this->m_sName, $iPos + 2);
$oObj = $aArgs[$sParamName.'->object()'];
if ($sAttCode == 'id')
{
return CMDBSource::Quote($oObj->GetKey());
}
return CMDBSource::Quote($oObj->Get($sAttCode));
}
}
if ($bRetrofitParams)
{
$aArgs[$this->m_sName] = null;
return ':'.$this->m_sName;
}
else
{
throw new MissingQueryArgument('Missing query argument', array('expecting'=>$this->m_sName, 'available'=>$aArgs));
throw new MissingQueryArgument('Missing query argument', array('expecting'=>$this->m_sName, 'available'=>array_keys($aArgs)));
}
}
@@ -632,12 +652,29 @@ class VariableExpression extends UnaryExpression
public function GetAsScalar($aArgs)
{
$value = '';
$value = null;
if (array_key_exists($this->m_sName, $aArgs))
{
$value = $aArgs[$this->m_sName];
}
else
elseif (($iPos = strpos($this->m_sName, '->')) !== false)
{
$sParamName = substr($this->m_sName, 0, $iPos);
if (array_key_exists($sParamName.'->object()', $aArgs))
{
$sAttCode = substr($this->m_sName, $iPos + 2);
$oObj = $aArgs[$sParamName.'->object()'];
if ($sAttCode == 'id')
{
$value = $oObj->GetKey();
}
else
{
$value = $oObj->Get($sAttCode);
}
}
}
if (is_null($value))
{
throw new MissingQueryArgument('Missing query argument', array('expecting'=>$this->m_sName, 'available'=>array_keys($aArgs)));
}
@@ -879,31 +916,63 @@ class FunctionExpression extends Expression
*/
public function MakeValueLabel($oFilter, $sValue, $sDefault)
{
static $aWeekDayToString = null;
if (is_null($aWeekDayToString))
{
// Init the correspondance table
$aWeekDayToString = array(
0 => Dict::S('DayOfWeek-Sunday'),
1 => Dict::S('DayOfWeek-Monday'),
2 => Dict::S('DayOfWeek-Tuesday'),
3 => Dict::S('DayOfWeek-Wednesday'),
4 => Dict::S('DayOfWeek-Thursday'),
5 => Dict::S('DayOfWeek-Friday'),
6 => Dict::S('DayOfWeek-Saturday')
);
}
static $aMonthToString = null;
if (is_null($aMonthToString))
{
// Init the correspondance table
$aMonthToString = array(
1 => Dict::S('Month-01'),
2 => Dict::S('Month-02'),
3 => Dict::S('Month-03'),
4 => Dict::S('Month-04'),
5 => Dict::S('Month-05'),
6 => Dict::S('Month-06'),
7 => Dict::S('Month-07'),
8 => Dict::S('Month-08'),
9 => Dict::S('Month-09'),
10 => Dict::S('Month-10'),
11 => Dict::S('Month-11'),
12 => Dict::S('Month-12'),
);
}
$sRes = $sDefault;
if (strtolower($this->m_sVerb) == 'date_format')
{
$oFormatExpr = $this->m_aArgs[1];
if ($oFormatExpr->Render() == "'%w'")
{
static $aWeekDayToString = null;
if (is_null($aWeekDayToString))
{
// Init the correspondance table
$aWeekDayToString = array(
0 => Dict::S('DayOfWeek-Sunday'),
1 => Dict::S('DayOfWeek-Monday'),
2 => Dict::S('DayOfWeek-Tuesday'),
3 => Dict::S('DayOfWeek-Wednesday'),
4 => Dict::S('DayOfWeek-Thursday'),
5 => Dict::S('DayOfWeek-Friday'),
6 => Dict::S('DayOfWeek-Saturday')
);
}
if (isset($aWeekDayToString[(int)$sValue]))
{
$sRes = $aWeekDayToString[(int)$sValue];
}
}
elseif ($oFormatExpr->Render() == "'%Y-%m'")
{
// yyyy-mm => "yyyy month"
$iMonth = (int) substr($sValue, -2); // the two last chars
$sRes = substr($sValue, 0, 4).' '.$aMonthToString[$iMonth];
}
elseif ($oFormatExpr->Render() == "'%Y-%m-%d'")
{
// yyyy-mm-dd => "month d"
$iMonth = (int) substr($sValue, 5, 2);
$sRes = $aMonthToString[$iMonth].' '.(int)substr($sValue, -2);
}
}
return $sRes;
}
@@ -1112,6 +1181,16 @@ class CharConcatWSExpression extends CharConcatExpression
$sSep = CMDBSource::Quote($this->m_separator);
return "CAST(CONCAT_WS($sSep, ".implode(', ', $aRes).") AS CHAR)";
}
public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
{
$aRes = array();
foreach ($this->m_aExpressions as $oExpr)
{
$aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
return new CharConcatWSExpression($this->m_separator, $aRes);
}
}

View File

@@ -28,70 +28,229 @@ class ExecutionKPI
{
static protected $m_bEnabled_Duration = false;
static protected $m_bEnabled_Memory = false;
static protected $m_bBlameCaller = false;
static protected $m_sAllowedUser = '*';
static protected $m_aStats = array();
static protected $m_aStats = array(); // Recurrent operations
static protected $m_aExecData = array(); // One shot operations
protected $m_fStarted = null;
protected $m_iInitialMemory = null;
static public function EnableDuration()
static public function EnableDuration($iLevel)
{
self::$m_bEnabled_Duration = true;
if ($iLevel > 0)
{
self::$m_bEnabled_Duration = true;
if ($iLevel > 1)
{
self::$m_bBlameCaller = true;
}
}
}
static public function EnableMemory()
static public function EnableMemory($iLevel)
{
self::$m_bEnabled_Memory = true;
if ($iLevel > 0)
{
self::$m_bEnabled_Memory = true;
}
}
/**
* @param string sUser A user login or * for all users
*/
static public function SetAllowedUser($sUser)
{
self::$m_sAllowedUser = $sUser;
}
static public function IsEnabled()
{
if (self::$m_bEnabled_Duration || self::$m_bEnabled_Memory)
{
if ((self::$m_sAllowedUser == '*') || (UserRights::GetUser() == trim(self::$m_sAllowedUser)))
{
return true;
}
}
return false;
}
static public function GetDescription()
{
$aFeatures = array();
if (self::$m_bEnabled_Duration) $aFeatures[] = 'Duration';
if (self::$m_bEnabled_Memory) $aFeatures[] = 'Memory usage';
$sFeatures = implode(', ', $aFeatures);
$sFor = self::$m_sAllowedUser == '*' ? 'EVERYBODY' : "'".trim(self::$m_sAllowedUser)."'";
return "KPI logging is active for $sFor. Measures: $sFeatures";
}
static public function ReportStats()
{
if (!self::IsEnabled()) return;
global $fItopStarted;
$sExecId = microtime(); // id to differentiate the hrefs!
$aBeginTimes = array();
foreach (self::$m_aExecData as $aOpStats)
{
$aBeginTimes[] = $aOpStats['time_begin'];
}
array_multisort($aBeginTimes, self::$m_aExecData);
$sTableStyle = 'background-color: #ccc; margin: 10px;';
self::Report("<hr/>");
self::Report("<div style=\"background-color: grey; padding: 10px;\">");
self::Report("<h3><a name=\"".md5($sExecId)."\">KPIs</a> - ".$_SERVER['REQUEST_URI']." (".$_SERVER['REQUEST_METHOD'].")</h3>");
self::Report("<p>".date('Y-m-d H:i:s', $fItopStarted)."</p>");
self::Report("<p>log_kpi_user_id: ".MetaModel::GetConfig()->Get('log_kpi_user_id')."</p>");
self::Report("<div>");
self::Report("<table border=\"1\" style=\"$sTableStyle\">");
self::Report("<thead>");
self::Report(" <th>Operation</th><th>Begin</th><th>End</th><th>Duration</th><th>Memory start</th><th>Memory end</th><th>Memory peak</th>");
self::Report("</thead>");
foreach (self::$m_aExecData as $aOpStats)
{
$sOperation = $aOpStats['op'];
$sBegin = $sEnd = $sDuration = $sMemBegin = $sMemEnd = $sMemPeak = '?';
$sBegin = round($aOpStats['time_begin'], 3);
$sEnd = round($aOpStats['time_end'], 3);
$fDuration = $aOpStats['time_end'] - $aOpStats['time_begin'];
$sDuration = round($fDuration, 3);
if (isset($aOpStats['mem_begin']))
{
$sMemBegin = self::MemStr($aOpStats['mem_begin']);
$sMemEnd = self::MemStr($aOpStats['mem_end']);
if (isset($aOpStats['mem_peak']))
{
$sMemPeak = self::MemStr($aOpStats['mem_peak']);
}
}
self::Report("<tr>");
self::Report(" <td>$sOperation</td><td>$sBegin</td><td>$sEnd</td><td>$sDuration</td><td>$sMemBegin</td><td>$sMemEnd</td><td>$sMemPeak</td>");
self::Report("</tr>");
}
self::Report("</table>");
self::Report("</div>");
$aConsolidatedStats = array();
foreach (self::$m_aStats as $sOperation => $aOpStats)
{
echo "<h2>KPIs for $sOperation</h2>\n";
$fTotalOp = 0;
$iTotalOp = 0;
$fMinOp = null;
$fMaxOp = 0;
echo "<ul>\n";
$sMaxOpArguments = null;
foreach ($aOpStats as $sArguments => $aEvents)
{
foreach ($aEvents as $aEventData)
{
$fDuration = $aEventData['time'];
$fTotalOp += $fDuration;
$iTotalOp++;
$fMinOp = is_null($fMinOp) ? $fDuration : min($fMinOp, $fDuration);
if ($fDuration > $fMaxOp)
{
$sMaxOpArguments = $sArguments;
$fMaxOp = $fDuration;
}
}
}
$aConsolidatedStats[$sOperation] = array(
'count' => $iTotalOp,
'duration' => $fTotalOp,
'min' => $fMinOp,
'max' => $fMaxOp,
'avg' => $fTotalOp / $iTotalOp,
'max_args' => $sMaxOpArguments
);
}
self::Report("<div>");
self::Report("<table border=\"1\" style=\"$sTableStyle\">");
self::Report("<thead>");
self::Report(" <th>Operation</th><th>Count</th><th>Duration</th><th>Min</th><th>Max</th><th>Avg</th>");
self::Report("</thead>");
foreach ($aConsolidatedStats as $sOperation => $aOpStats)
{
$sOperation = '<a href="#'.md5($sExecId.$sOperation).'">'.$sOperation.'</a>';
$sCount = $aOpStats['count'];
$sDuration = round($aOpStats['duration'], 3);
$sMin = round($aOpStats['min'], 3);
$sMax = '<a href="#'.md5($sExecId.$aOpStats['max_args']).'">'.round($aOpStats['max'], 3).'</a>';
$sAvg = round($aOpStats['avg'], 3);
self::Report("<tr>");
self::Report(" <td>$sOperation</td><td>$sCount</td><td>$sDuration</td><td>$sMin</td><td>$sMax</td><td>$sAvg</td>");
self::Report("</tr>");
}
self::Report("</table>");
self::Report("</div>");
self::Report("</div>");
// Report operation details
foreach (self::$m_aStats as $sOperation => $aOpStats)
{
$sOperationHtml = '<a name="'.md5($sExecId.$sOperation).'">'.$sOperation.'</a>';
self::Report("<h4>$sOperationHtml</h4>");
self::Report("<p><a href=\"#".md5($sExecId)."\">Back to page stats</a></p>");
self::Report("<table border=\"1\" style=\"$sTableStyle\">");
self::Report("<thead>");
self::Report(" <th>Operation details (+ blame caller if log_kpi_duration = 2)</th><th>Count</th><th>Duration</th><th>Min</th><th>Max</th>");
self::Report("</thead>");
foreach ($aOpStats as $sArguments => $aEvents)
{
$sHtmlArguments = '<a name="'.md5($sExecId.$sArguments).'"><div style="white-space: pre-wrap;">'.$sArguments.'</div></a>';
if ($aConsolidatedStats[$sOperation]['max_args'] == $sArguments)
{
$sHtmlArguments = '<span style="color: red;">'.$sHtmlArguments.'</span>';
}
if (isset($aEvents[0]['callers']))
{
$sHtmlArguments .= '<div style="padding: 10px;">';
$sHtmlArguments .= '<table border="1" bgcolor="#cfc">';
$sHtmlArguments .= '<tr><td colspan="2" bgcolor="#e9b96">Call stack for the <b>FIRST</b> caller</td></tr>';
foreach ($aEvents[0]['callers'] as $aCall)
{
$sHtmlArguments .= '<tr>';
$sHtmlArguments .= '<td>'.$aCall['Function'].'</td>';
$sHtmlArguments .= '<td>'.$aCall['File'].':'.$aCall['Line'].'</td>';
$sHtmlArguments .= '</tr>';
}
$sHtmlArguments .= '</table>';
$sHtmlArguments .= '</div>';
}
$fTotalInter = 0;
$fMinInter = null;
$fMaxInter = 0;
foreach ($aEvents as $fDuration)
foreach ($aEvents as $aEventData)
{
$fDuration = $aEventData['time'];
$fTotalInter += $fDuration;
$fMinInter = is_null($fMinInter) ? $fDuration : min($fMinInter, $fDuration);
$fMaxInter = max($fMaxInter, $fDuration);
$fMinOp = is_null($fMinOp) ? $fDuration : min($fMinOp, $fDuration);
$fMaxOp = max($fMaxOp, $fDuration);
}
$fTotalOp += $fTotalInter;
$iTotalOp++;
$iCountInter = count($aEvents);
$sTotalInter = round($fTotalInter, 3)."s";
if ($iCountInter > 1)
{
$sMinInter = round($fMinInter, 3)."s";
$sMaxInter = round($fMaxInter, 3)."s";
$sTimeDesc = "$sTotalInter (from $sMinInter to $sMaxInter) in $iCountInter times";
}
else
{
$sTimeDesc = "$sTotalInter";
}
echo "<li>Spent $sTimeDesc, on: <span style=\"font-size:60%\">$sArguments</span></li>\n";
$sTotalInter = round($fTotalInter, 3);
$sMinInter = round($fMinInter, 3);
$sMaxInter = round($fMaxInter, 3);
self::Report("<tr>");
self::Report(" <td>$sHtmlArguments</td><td>$iCountInter</td><td>$sTotalInter</td><td>$sMinInter</td><td>$sMaxInter</td>");
self::Report("</tr>");
}
echo "</ul>\n";
echo "<ul>Sumary for $sOperation\n";
echo "<li>Total: $iTotalOp (".round($fTotalOp, 3).")</li>\n";
echo "<li>Min: ".round($fMinOp, 3)."</li>\n";
echo "<li>Max: ".round($fMaxOp, 3)."</li>\n";
echo "<li>Avg: ".round($fTotalOp / $iTotalOp, 3)."</li>\n";
echo "</ul>\n";
self::Report("</table>");
}
}
@@ -105,25 +264,43 @@ class ExecutionKPI
//
public function ComputeAndReport($sOperationDesc)
{
global $fItopStarted;
$aNewEntry = null;
if (self::$m_bEnabled_Duration)
{
$fStopped = MyHelpers::getmicrotime();
$fDuration = $fStopped - $this->m_fStarted;
$this->Report($sOperationDesc.' / duration: '.round($fDuration, 3));
$aNewEntry = array(
'op' => $sOperationDesc,
'time_begin' => $this->m_fStarted - $fItopStarted,
'time_end' => $fStopped - $fItopStarted,
);
// Reset for the next operation (if the object is recycled)
$this->m_fStarted = $fStopped;
}
if (self::$m_bEnabled_Memory)
{
$iMemory = self::memory_get_usage();
$iMemoryUsed = $iMemory - $this->m_iInitialMemory;
$this->Report($sOperationDesc.' / memory: '.self::MemStr($iMemoryUsed).' (Total: '.self::MemStr($iMemory).')');
$iCurrentMemory = self::memory_get_usage();
if (is_null($aNewEntry))
{
$aNewEntry = array('op' => $sOperationDesc);
}
$aNewEntry['mem_begin'] = $this->m_iInitialMemory;
$aNewEntry['mem_end'] = $iCurrentMemory;
if (function_exists('memory_get_peak_usage'))
{
$iMemoryPeak = memory_get_peak_usage();
$this->Report($sOperationDesc.' / memory peak: '.self::MemStr($iMemoryPeak));
$aNewEntry['mem_peak'] = memory_get_peak_usage();
}
// Reset for the next operation (if the object is recycled)
$this->m_iInitialMemory = $iCurrentMemory;
}
if (!is_null($aNewEntry))
{
self::$m_aExecData[] = $aNewEntry;
}
$this->ResetCounters();
}
@@ -133,7 +310,19 @@ class ExecutionKPI
{
$fStopped = MyHelpers::getmicrotime();
$fDuration = $fStopped - $this->m_fStarted;
self::$m_aStats[$sOperation][$sArguments][] = $fDuration;
if (self::$m_bBlameCaller)
{
self::$m_aStats[$sOperation][$sArguments][] = array(
'time' => $fDuration,
'callers' => MyHelpers::get_callstack(1),
);
}
else
{
self::$m_aStats[$sOperation][$sArguments][] = array(
'time' => $fDuration
);
}
}
}
@@ -150,9 +339,11 @@ class ExecutionKPI
}
}
protected function Report($sText)
const HtmlReportFile = 'log/kpi.html';
static protected function Report($sText)
{
echo "$sText<br/>\n";
file_put_contents(APPROOT.self::HtmlReportFile, "$sText\n", FILE_APPEND | LOCK_EX);
}
static protected function MemStr($iMemory)
@@ -204,13 +395,3 @@ class ExecutionKPI
}
}
class ApplicationStartupKPI extends ExecutionKPI
{
public function __construct()
{
global $fItopStarted;
$this->m_fStarted = $fItopStarted;
}
}
?>

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -174,7 +174,7 @@ abstract class MetaModel
// (it is not possible to guess it when called as myderived::...)
if (!array_key_exists($sClass, self::$m_aClassParams))
{
throw new CoreException("Unknown class '$sClass', expected a value in {".implode(', ', array_keys(self::$m_aClassParams))."}");
throw new CoreException("Unknown class '$sClass'");
}
}
@@ -593,15 +593,10 @@ abstract class MetaModel
$oAtt = self::GetAttributeDef($sClass, $sAttCode);
// Temporary implementation: later, we might be able to compute
// the dependencies, based on the attributes definition
// (allowed values and default values)
if ($oAtt->IsWritable())
{
return $oAtt->GetPrerequisiteAttributes();
}
else
{
return array();
}
// (allowed values and default values)
// Even non-writable attributes (like ExternalFields) can now have Prerequisites
return $oAtt->GetPrerequisiteAttributes();
}
/**
* Find all attributes that depend on the specified one (reverse of GetPrequisiteAttributes)
@@ -671,6 +666,19 @@ abstract class MetaModel
return $aTables;
}
final static public function DBGetIndexes($sClass)
{
self::_check_subclass($sClass);
if (isset(self::$m_aClassParams[$sClass]['indexes']))
{
return self::$m_aClassParams[$sClass]['indexes'];
}
else
{
return array();
}
}
final static public function DBGetKey($sClass)
{
self::_check_subclass($sClass);
@@ -858,15 +866,32 @@ abstract class MetaModel
final static public function GetExternalFields($sClass, $sKeyAttCode)
{
$aExtFields = array();
foreach (self::ListAttributeDefs($sClass) as $sAttCode => $oAtt)
static $aExtFields = array();
if (!isset($aExtFields[$sClass][$sKeyAttCode]))
{
if ($oAtt->IsExternalField() && ($oAtt->GetKeyAttCode() == $sKeyAttCode))
$aExtFields[$sClass][$sKeyAttCode] = array();
foreach (self::ListAttributeDefs($sClass) as $sAttCode => $oAtt)
{
$aExtFields[] = $oAtt;
if ($oAtt->IsExternalField() && ($oAtt->GetKeyAttCode() == $sKeyAttCode))
{
$aExtFields[$sClass][$sKeyAttCode][$oAtt->GetExtAttCode()] = $oAtt;
}
}
}
return $aExtFields;
return $aExtFields[$sClass][$sKeyAttCode];
}
final static public function FindExternalField($sClass, $sKeyAttCode, $sRemoteAttCode)
{
$aExtFields = self::GetExternalFields($sClass, $sKeyAttCode);
if (isset($aExtFields[$sRemoteAttCode]))
{
return $aExtFields[$sRemoteAttCode];
}
else
{
return null;
}
}
final static public function GetExtKeyFriends($sClass, $sExtKeyAttCode)
@@ -913,9 +938,10 @@ abstract class MetaModel
* Get the attribute label
* @param string sClass Persistent class
* @param string sAttCodeEx Extended attribute code: attcode[->attcode]
* @param bool $bShowMandatory If true, add a star character (at the end or before the ->) to show that the field is mandatory
* @return string A user friendly format of the string: AttributeName or AttributeName->ExtAttributeName
*/
public static function GetLabel($sClass, $sAttCodeEx)
public static function GetLabel($sClass, $sAttCodeEx, $bShowMandatory = false)
{
$sLabel = '';
if (preg_match('/(.+)->(.+)/', $sAttCodeEx, $aMatches) > 0)
@@ -923,16 +949,17 @@ abstract class MetaModel
$sAttribute = $aMatches[1];
$sField = $aMatches[2];
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttribute);
$sMandatory = ($bShowMandatory && !$oAttDef->IsNullAllowed()) ? '*' : '';
if ($oAttDef->IsExternalKey())
{
$sTargetClass = $oAttDef->GetTargetClass();
$oTargetAttDef = MetaModel::GetAttributeDef($sTargetClass, $sField);
$sLabel = $oAttDef->GetLabel().'->'.$oTargetAttDef->GetLabel();
$sLabel = $oAttDef->GetLabel().$sMandatory.'->'.$oTargetAttDef->GetLabel();
}
else
{
// Let's return something displayable... but this should never happen!
$sLabel = $oAttDef->GetLabel().'->'.$aMatches[2];
$sLabel = $oAttDef->GetLabel().$sMandatory.'->'.$aMatches[2];
}
}
else
@@ -944,7 +971,8 @@ abstract class MetaModel
else
{
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCodeEx);
$sLabel = $oAttDef->GetLabel();
$sMandatory = ($bShowMandatory && !$oAttDef->IsNullAllowed()) ? '*' : '';
$sLabel = $oAttDef->GetLabel().$sMandatory;
}
}
return $sLabel;
@@ -1967,15 +1995,30 @@ abstract class MetaModel
{
return array_unique(self::$m_aRootClasses);
}
public static function EnumParentClasses($sClass, $iOption = ENUM_PARENT_CLASSES_EXCLUDELEAF)
public static function EnumParentClasses($sClass, $iOption = ENUM_PARENT_CLASSES_EXCLUDELEAF, $bRootFirst = true)
{
self::_check_subclass($sClass);
if ($iOption == ENUM_PARENT_CLASSES_EXCLUDELEAF)
if ($bRootFirst)
{
return self::$m_aParentClasses[$sClass];
$aRes = self::$m_aParentClasses[$sClass];
}
else
{
$aRes = array_reverse(self::$m_aParentClasses[$sClass], true);
}
if ($iOption != ENUM_PARENT_CLASSES_EXCLUDELEAF)
{
if ($bRootFirst)
{
// Leaf class at the end
$aRes[] = $sClass;
}
else
{
// Leaf class on top
array_unshift($aRes, $sClass);
}
}
$aRes = self::$m_aParentClasses[$sClass];
$aRes[] = $sClass;
return $aRes;
}
public static function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP)
@@ -2062,24 +2105,52 @@ abstract class MetaModel
{
if (self::IsValidObject($value))
{
$aScalarArgs = array_merge($aScalarArgs, $value->ToArgs($sArgName));
if (strpos($sArgName, '->object()') === false)
{
// Lazy syntax - develop the object contextual parameters
$aScalarArgs = array_merge($aScalarArgs, $value->ToArgsForQuery($sArgName));
}
else
{
// Leave as is
$aScalarArgs[$sArgName] = $value;
}
}
else
{
$aScalarArgs[$sArgName] = (string) $value;
if (is_scalar($value))
{
$aScalarArgs[$sArgName] = (string) $value;
}
}
}
// Add standard contextual arguments
//
$aScalarArgs['current_contact_id'] = UserRights::GetContactId();
return $aScalarArgs;
}
public static function MakeGroupByQuery(DBObjectSearch $oFilter, $aArgs, $aGroupByExpr)
public static function MakeGroupByQuery(DBObjectSearch $oFilter, $aArgs, $aGroupByExpr, $bExcludeNullValues = false)
{
$aAttToLoad = array();
$oSelect = self::MakeSelectStructure($oFilter, array(), $aArgs, $aAttToLoad, null, 0, 0, false, $aGroupByExpr);
if ($bExcludeNullValues)
{
// Null values are not handled (though external keys set to 0 are allowed)
$oQueryFilter = $oFilter->DeepClone();
foreach ($aGroupByExpr as $oGroupByExp)
{
$oNull = new FunctionExpression('ISNULL', array($oGroupByExp));
$oNotNull = new BinaryExpression($oNull, '!=', new TrueExpression());
$oQueryFilter->AddConditionExpression($oNotNull);
}
}
else
{
$oQueryFilter = $oFilter;
}
$oSelect = self::MakeSelectStructure($oQueryFilter, array(), $aArgs, $aAttToLoad, null, 0, 0, false, $aGroupByExpr);
$aScalarArgs = array_merge(self::PrepareQueryArguments($aArgs), $oFilter->GetInternalParams());
try
@@ -3263,7 +3334,7 @@ abstract class MetaModel
{
if(!self::IsValidAttCode($sClass, $sAttCode))
{
$aErrors[$sClass][] = "Unkown attribute code '".$sAttCode."' for the name definition";
$aErrors[$sClass][] = "Unknown attribute code '".$sAttCode."' for the name definition";
$aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass));
}
}
@@ -3272,7 +3343,7 @@ abstract class MetaModel
{
if (!empty($sReconcKeyAttCode) && !self::IsValidAttCode($sClass, $sReconcKeyAttCode))
{
$aErrors[$sClass][] = "Unkown attribute code '".$sReconcKeyAttCode."' in the list of reconciliation keys";
$aErrors[$sClass][] = "Unknown attribute code '".$sReconcKeyAttCode."' in the list of reconciliation keys";
$aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass));
}
}
@@ -3287,7 +3358,7 @@ abstract class MetaModel
{
if (!self::IsValidClass($oAttDef->GetTargetClass()))
{
$aErrors[$sClass][] = "Unkown class '".$oAttDef->GetTargetClass()."' for the external key '$sAttCode'";
$aErrors[$sClass][] = "Unknown class '".$oAttDef->GetTargetClass()."' for the external key '$sAttCode'";
$aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetClasses())."}";
}
}
@@ -3296,7 +3367,7 @@ abstract class MetaModel
$sKeyAttCode = $oAttDef->GetKeyAttCode();
if (!self::IsValidAttCode($sClass, $sKeyAttCode) || !self::IsValidKeyAttCode($sClass, $sKeyAttCode))
{
$aErrors[$sClass][] = "Unkown key attribute code '".$sKeyAttCode."' for the external field $sAttCode";
$aErrors[$sClass][] = "Unknown key attribute code '".$sKeyAttCode."' for the external field $sAttCode";
$aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetKeysList($sClass))."}";
}
else
@@ -3306,11 +3377,15 @@ abstract class MetaModel
$sExtAttCode = $oAttDef->GetExtAttCode();
if (!self::IsValidAttCode($sTargetClass, $sExtAttCode))
{
$aErrors[$sClass][] = "Unkown key attribute code '".$sExtAttCode."' for the external field $sAttCode";
$aErrors[$sClass][] = "Unknown key attribute code '".$sExtAttCode."' for the external field $sAttCode";
$aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetKeysList($sTargetClass))."}";
}
}
}
else if ($oAttDef->IsLinkSet())
{
// Do nothing...
}
else // standard attributes
{
// Check that the default values definition is a valid object!
@@ -3343,7 +3418,7 @@ abstract class MetaModel
{
if (!self::IsValidAttCode($sClass, $sDependOnAttCode))
{
$aErrors[$sClass][] = "Unkown attribute code '".$sDependOnAttCode."' in the list of prerequisite attributes";
$aErrors[$sClass][] = "Unknown attribute code '".$sDependOnAttCode."' in the list of prerequisite attributes";
$aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass));
}
}
@@ -3370,7 +3445,7 @@ abstract class MetaModel
// Lifecycle - check that the state attribute does exist as an attribute
if (!self::IsValidAttCode($sClass, $sStateAttCode))
{
$aErrors[$sClass][] = "Unkown attribute code '".$sStateAttCode."' for the state definition";
$aErrors[$sClass][] = "Unknown attribute code '".$sStateAttCode."' for the state definition";
$aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetAttributesList($sClass))."}";
}
else
@@ -3444,7 +3519,7 @@ abstract class MetaModel
{
if (!self::IsValidAttCode($sClass, $sMyAttCode))
{
$aErrors[$sClass][] = "Unkown attribute code '".$sMyAttCode."' from ZList '$sListCode'";
$aErrors[$sClass][] = "Unknown attribute code '".$sMyAttCode."' from ZList '$sListCode'";
$aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetAttributesList($sClass))."}";
}
}
@@ -3903,7 +3978,7 @@ abstract class MetaModel
$aCreateTableItems[$sTable][$sField] = $sFieldDefinition;
if ($bIndexNeeded)
{
$aCreateTableItems[$sTable][$sField.'_ix'] = "INDEX (`$sField`)";
$aCreateTableItems[$sTable][] = "INDEX (`$sField`)";
}
}
else
@@ -3911,7 +3986,7 @@ abstract class MetaModel
$aAlterTableItems[$sTable][$sField] = "ADD $sFieldDefinition";
if ($bIndexNeeded)
{
$aAlterTableItems[$sTable][$sField.'_ix'] = "ADD INDEX (`$sField`)";
$aAlterTableItems[$sTable][] = "ADD INDEX (`$sField`)";
}
}
}
@@ -3946,15 +4021,58 @@ abstract class MetaModel
// Create indexes (external keys only... so far)
//
if ($bIndexNeeded && !CMDBSource::HasIndex($sTable, $sField))
if ($bIndexNeeded && !CMDBSource::HasIndex($sTable, $sField, array($sField)))
{
$aErrors[$sClass][$sAttCode][] = "Foreign key '$sField' in table '$sTable' should have an index";
$aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` ADD INDEX (`$sField`)";
$aAlterTableItems[$sTable][$sField.'_ix'] = "ADD INDEX (`$sField`)";
if (CMDBSource::HasIndex($sTable, $sField))
{
$aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` DROP INDEX `$sField`, ADD INDEX (`$sField`)";
$aAlterTableItems[$sTable][] = "DROP INDEX `$sField`";
$aAlterTableItems[$sTable][] = "ADD INDEX (`$sField`)";
}
else
{
$aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` ADD INDEX (`$sField`)";
$aAlterTableItems[$sTable][] = "ADD INDEX (`$sField`)";
}
}
}
}
}
// Check indexes
foreach (self::DBGetIndexes($sClass) as $aColumns)
{
$sIndexId = implode('_', $aColumns);
if(!CMDBSource::HasIndex($sTable, $sIndexId, $aColumns))
{
$sColumns = "`".implode("`, `", $aColumns)."`";
if (CMDBSource::HasIndex($sTable, $sIndexId))
{
$aErrors[$sClass]['*'][] = "Wrong index '$sIndexId' ($sColumns) in table '$sTable'";
$aSugFix[$sClass]['*'][] = "ALTER TABLE `$sTable` DROP INDEX `$sIndexId`, ADD INDEX `$sIndexId` ($sColumns)";
}
else
{
$aErrors[$sClass]['*'][] = "Missing index '$sIndexId' ($sColumns) in table '$sTable'";
$aSugFix[$sClass]['*'][] = "ALTER TABLE `$sTable` ADD INDEX `$sIndexId` ($sColumns)";
}
if (array_key_exists($sTable, $aCreateTable))
{
$aCreateTableItems[$sTable][] = "INDEX `$sIndexId` ($sColumns)";
}
else
{
if (CMDBSource::HasIndex($sTable, $sIndexId))
{
$aAlterTableItems[$sTable][] = "DROP INDEX `$sIndexId`";
}
$aAlterTableItems[$sTable][] = "ADD INDEX `$sIndexId` ($sColumns)";
}
}
}
// Find out unused columns
//
foreach($aTableInfo['Fields'] as $sField => $aFieldData)
@@ -4496,14 +4614,9 @@ abstract class MetaModel
self::$m_bLogWebService = false;
}
if (self::$m_oConfig->GetLogKPIDuration())
{
ExecutionKPI::EnableDuration();
}
if (self::$m_oConfig->GetLogKPIMemory())
{
ExecutionKPI::EnableMemory();
}
ExecutionKPI::EnableDuration(self::$m_oConfig->Get('log_kpi_duration'));
ExecutionKPI::EnableMemory(self::$m_oConfig->Get('log_kpi_memory'));
ExecutionKPI::SetAllowedUser(self::$m_oConfig->Get('log_kpi_user_id'));
self::$m_bTraceQueries = self::$m_oConfig->GetLogQueries();
self::$m_bIndentQueries = self::$m_oConfig->Get('query_indentation_enabled');
@@ -4969,6 +5082,41 @@ abstract class MetaModel
}
}
/**
* Helper to remove selected objects without calling any handler
* Surpasses BulkDelete as it can handle abstract classes, but has the other limitation as it bypasses standard objects handlers
*
* @param string $oFilter Scope of objects to wipe out
* @return The count of deleted objects
*/
public static function PurgeData($oFilter)
{
$sTargetClass = $oFilter->GetClass();
$oSet = new DBObjectSet($oFilter);
$oSet->OptimizeColumnLoad(array($sTargetClass => array('finalclass')));
$aIdToClass = $oSet->GetColumnAsArray('finalclass', true);
$aIds = array_keys($aIdToClass);
if (count($aIds) > 0)
{
$aQuotedIds = CMDBSource::Quote($aIds);
$sIdList = implode(',', $aQuotedIds);
$aTargetClasses = array_merge(
self::EnumChildClasses($sTargetClass, ENUM_CHILD_CLASSES_ALL),
self::EnumParentClasses($sTargetClass, ENUM_PARENT_CLASSES_EXCLUDELEAF)
);
foreach ($aTargetClasses as $sSomeClass)
{
$sTable = MetaModel::DBGetTable($sSomeClass);
$sPKField = MetaModel::DBGetKey($sSomeClass);
$sDeleteSQL = "DELETE FROM `$sTable` WHERE `$sPKField` IN ($sIdList)";
CMDBSource::DeleteFrom($sDeleteSQL);
}
}
return count($aIds);
}
// Links
//
//
@@ -5020,6 +5168,12 @@ abstract class MetaModel
}
return $aResult;
}
/**
* It is not recommended to use this function: call GetLinkClasses instead
* Return classes having at least to external keys (thus too many classes as compared to GetLinkClasses)
* The only difference with EnumLinkingClasses is the output format
*/
public static function EnumLinksClasses()
{
// Returns a flat array of classes having at least two external keys
@@ -5042,6 +5196,11 @@ abstract class MetaModel
}
return $aResult;
}
/**
* It is not recommended to use this function: call GetLinkClasses instead
* Return classes having at least to external keys (thus too many classes as compared to GetLinkClasses)
* The only difference with EnumLinksClasses is the output format
*/
public static function EnumLinkingClasses($sClass = "")
{
// N-N links, array of sLinkClass => (array of sAttCode=>sClass)
@@ -5078,6 +5237,38 @@ abstract class MetaModel
return $aResult;
}
/**
* This function has two siblings that will be soon deprecated:
* EnumLinkingClasses and EnumLinkClasses
*
* Using GetLinkClasses is the recommended way to determine if a class is
* actually an N-N relation because it is based on the decision made by the
* designer the data model
*/
public static function GetLinkClasses()
{
$aRet = array();
foreach(self::GetClasses() as $sClass)
{
if (isset(self::$m_aClassParams[$sClass]["is_link"]))
{
if (self::$m_aClassParams[$sClass]["is_link"])
{
$aExtKeys = array();
foreach (self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
{
if ($oAttDef->IsExternalKey())
{
$aExtKeys[$sAttCode] = $oAttDef->GetTargetClass();
}
}
$aRet[$sClass] = $aExtKeys;
}
}
}
return $aRet;
}
public static function GetLinkLabel($sLinkClass, $sAttCode)
{
self::_check_subclass($sLinkClass);
@@ -5147,18 +5338,27 @@ abstract class MetaModel
$sEnvironment = MetaModel::GetEnvironmentId();
}
$aEntries = array();
$aCacheUserData = @apc_cache_info('user');
if (is_array($aCacheUserData))
if (extension_loaded('apcu'))
{
// Beware: APCu behaves slightly differently from APC !!
$aCacheUserData = @apc_cache_info();
}
else
{
$aCacheUserData = @apc_cache_info('user');
}
if (is_array($aCacheUserData) && isset($aCacheUserData['cache_list']))
{
$sPrefix = 'itop-'.$sEnvironment.'-';
foreach($aCacheUserData['cache_list'] as $i => $aEntry)
{
$sEntryKey = $aEntry['info'];
$sEntryKey = array_key_exists('info', $aEntry) ? $aEntry['info'] : $aEntry['key'];
if (strpos($sEntryKey, $sPrefix) === 0)
{
$sCleanKey = substr($sEntryKey, strlen($sPrefix));
$aEntries[$sCleanKey] = $aEntry;
$aEntries[$sCleanKey]['info'] = $sEntryKey;
}
}
}

View File

@@ -0,0 +1,261 @@
<?php
// Copyright (C) 2013 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/>
/**
* Reflection API for the MetaModel (partial)
*
* @copyright Copyright (C) 2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
abstract class ModelReflection
{
abstract public function GetClassIcon($sClass, $bImgTag = true);
abstract public function IsValidAttCode($sClass, $sAttCode);
abstract public function GetName($sClass);
abstract public function GetLabel($sClass, $sAttCodeEx);
abstract public function GetValueLabel($sClass, $sAttCode, $sValue);
abstract public function ListAttributes($sClass, $sScope = null);
abstract public function GetAttributeProperty($sClass, $sAttCode, $sPropName, $default = null);
abstract public function GetAllowedValues_att($sClass, $sAttCode);
abstract public function HasChildrenClasses($sClass);
abstract public function GetClasses($sCategories = '', $bExcludeLinks = false);
abstract public function IsValidClass($sClass);
abstract public function IsSameFamilyBranch($sClassA, $sClassB);
abstract public function GetParentClass($sClass);
abstract public function GetFiltersList($sClass);
abstract public function IsValidFilterCode($sClass, $sFilterCode);
abstract public function GetQuery($sOQL);
abstract public function DictString($sStringCode, $sDefault = null, $bUserLanguageOnly = false);
public function DictFormat($sFormatCode /*, ... arguments ....*/)
{
$sLocalizedFormat = $this->DictString($sFormatCode);
$aArguments = func_get_args();
array_shift($aArguments);
if ($sLocalizedFormat == $sFormatCode)
{
// Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded)
return $sFormatCode.' - '.implode(', ', $aArguments);
}
return vsprintf($sLocalizedFormat, $aArguments);
}
abstract public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = '');
}
abstract class QueryReflection
{
/**
* Throws an exception in case of an invalid syntax
*/
abstract public function __construct($sOQL);
abstract public function GetClass();
abstract public function GetClassAlias();
}
class ModelReflectionRuntime extends ModelReflection
{
public function __construct()
{
}
public function GetClassIcon($sClass, $bImgTag = true)
{
return MetaModel::GetClassIcon($sClass, $bImgTag);
}
public function IsValidAttCode($sClass, $sAttCode)
{
return MetaModel::IsValidAttCode($sClass, $sAttCode);
}
public function GetName($sClass)
{
return MetaModel::GetName($sClass);
}
public function GetLabel($sClass, $sAttCodeEx)
{
return MetaModel::GetLabel($sClass, $sAttCodeEx);
}
public function GetValueLabel($sClass, $sAttCode, $sValue)
{
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
return $oAttDef->GetValueLabel($sValue);
}
public function ListAttributes($sClass, $sScope = null)
{
$aScope = null;
if ($sScope != null)
{
$aScope = array();
foreach (explode(',', $sScope) as $sScopeClass)
{
$aScope[] = trim($sScopeClass);
}
}
$aAttributes = array();
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
{
$sAttributeClass = get_class($oAttDef);
if ($aScope != null)
{
foreach ($aScope as $sScopeClass)
{
if (($sAttributeClass == $sScopeClass) || is_subclass_of($sAttributeClass, $sScopeClass))
{
$aAttributes[$sAttCode] = $sAttributeClass;
break;
}
}
}
else
{
$aAttributes[$sAttCode] = $sAttributeClass;
}
}
return $aAttributes;
}
public function GetAttributeProperty($sClass, $sAttCode, $sPropName, $default = null)
{
$ret = $default;
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
$aParams = $oAttDef->GetParams();
if (array_key_exists($sPropName, $aParams))
{
$ret = $aParams[$sPropName];
}
if ($oAttDef instanceof AttributeHierarchicalKey)
{
if ($sPropName == 'targetclass')
{
$ret = $sClass;
}
}
return $ret;
}
public function GetAllowedValues_att($sClass, $sAttCode)
{
return MetaModel::GetAllowedValues_att($sClass, $sAttCode);
}
public function HasChildrenClasses($sClass)
{
return MetaModel::HasChildrenClasses($sClass);
}
public function GetClasses($sCategories = '', $bExcludeLinks = false)
{
$aClasses = MetaModel::GetClasses($sCategories);
if ($bExcludeLinks)
{
$aExcluded = MetaModel::GetLinkClasses();
$aRes = array();
foreach ($aClasses as $sClass)
{
if (!array_key_exists($sClass, $aExcluded))
{
$aRes[] = $sClass;
}
}
}
else
{
$aRes = $aClasses;
}
return $aRes;
}
public function IsValidClass($sClass)
{
return MetaModel::IsValidClass($sClass);
}
public function IsSameFamilyBranch($sClassA, $sClassB)
{
return MetaModel::IsSameFamilyBranch($sClassA, $sClassB);
}
public function GetParentClass($sClass)
{
return MetaModel::GetParentClass($sClass);
}
public function GetFiltersList($sClass)
{
return MetaModel::GetFiltersList($sClass);
}
public function IsValidFilterCode($sClass, $sFilterCode)
{
return MetaModel::IsValidFilterCode($sClass, $sFilterCode);
}
public function GetQuery($sOQL)
{
return new QueryReflectionRuntime($sOQL);
}
public function DictString($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
{
return Dict::S($sStringCode, $sDefault, $bUserLanguageOnly);
}
public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = '')
{
return new RunTimeIconSelectionField($sCode, $sLabel, $defaultValue);
}
}
class QueryReflectionRuntime extends QueryReflection
{
protected $oFilter;
/**
* throws an exception in case of a wrong syntax
*/
public function __construct($sOQL)
{
$this->oFilter = DBObjectSearch::FromOQL($sOQL);
}
public function GetClass()
{
return $this->oFilter->GetClass();
}
public function GetClassAlias()
{
return $this->oFilter->GetClassAlias();
}
}

193
core/mutex.class.inc.php Normal file
View File

@@ -0,0 +1,193 @@
<?php
// Copyright (C) 2013 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/>
/**
* Class iTopMutex
* A class to serialize the execution of some code sections
* Emulates the API of PECL Mutex class
* Relies on MySQL locks because the API sem_get is not always present in the
* installed PHP.
*
* @copyright Copyright (C) 2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
class iTopMutex
{
protected $sName;
protected $hDBLink;
protected $bLocked; // Whether or not this instance of the Mutex is locked
static protected $aAcquiredLocks = array(); // Number of instances of the Mutex, having the lock, in this page
public function __construct($sName, $sDBHost = null, $sDBUser = null, $sDBPwd = null)
{
// Compute the name of a lock for mysql
// Note: the name is server-wide!!!
$this->sName = 'itop.'.$sName;
$this->bLocked = false; // Not yet locked
if (!array_key_exists($this->sName, self::$aAcquiredLocks))
{
self::$aAcquiredLocks[$this->sName] = 0;
}
// It is a MUST to create a dedicated session each time a lock is required, because
// using GET_LOCK anytime on the same session will RELEASE the current and unique session lock (known issue)
$oConfig = utils::GetConfig();
$sDBHost = is_null($sDBHost) ? $oConfig->GetDBHost() : $sDBHost;
$sDBUser = is_null($sDBUser) ? $oConfig->GetDBUser() : $sDBUser;
$sDBPwd = is_null($sDBPwd) ? $oConfig->GetDBPwd() : $sDBPwd;
$this->InitMySQLSession($sDBHost, $sDBUser, $sDBPwd);
}
public function __destruct()
{
if ($this->bLocked)
{
$this->Unlock();
}
mysqli_close($this->hDBLink);
}
/**
* Acquire the mutex
*/
public function Lock()
{
if ($this->bLocked)
{
// Lock already acquired
return;
}
if (self::$aAcquiredLocks[$this->sName] == 0)
{
do
{
$res = $this->QueryToScalar("SELECT GET_LOCK('".$this->sName."', 3600)");
if (is_null($res))
{
throw new Exception("Failed to acquire the lock '".$this->sName."'");
}
// $res === '1' means I hold the lock
// $res === '0' means it timed out
}
while ($res !== '1');
}
$this->bLocked = true;
self::$aAcquiredLocks[$this->sName]++;
}
/**
* Attempt to acquire the mutex
* @returns bool True if the mutex is acquired, false if already locked elsewhere
*/
public function TryLock()
{
if ($this->bLocked)
{
return true; // Already acquired
}
if (self::$aAcquiredLocks[$this->sName] > 0)
{
self::$aAcquiredLocks[$this->sName]++;
$this->bLocked = true;
return true;
}
$res = $this->QueryToScalar("SELECT GET_LOCK('".$this->sName."', 0)");
if (is_null($res))
{
throw new Exception("Failed to acquire the lock '".$this->sName."'");
}
// $res === '1' means I hold the lock
// $res === '0' means it timed out
if ($res === '1')
{
$this->bLocked = true;
self::$aAcquiredLocks[$this->sName]++;
}
return ($res === '1');
}
/**
* Release the mutex
*/
public function Unlock()
{
if (!$this->bLocked)
{
// ??? the lock is not acquired, exit
return;
}
if (self::$aAcquiredLocks[$this->sName] == 0)
{
return; // Safety net
}
if (self::$aAcquiredLocks[$this->sName] == 1)
{
$res = $this->QueryToScalar("SELECT RELEASE_LOCK('".$this->sName."')");
}
$this->bLocked = false;
self::$aAcquiredLocks[$this->sName]--;
}
public function InitMySQLSession($sHost, $sUser, $sPwd)
{
$aConnectInfo = explode(':', $sHost);
if (count($aConnectInfo) > 1)
{
// Override the default port
$sServer = $aConnectInfo[0];
$iPort = $aConnectInfo[1];
$this->hDBLink = @mysqli_connect($sServer, $sUser, $sPwd, '', $iPort);
}
else
{
$this->hDBLink = @mysqli_connect($sHost, $sUser, $sPwd);
}
if (!$this->hDBLink)
{
throw new Exception("Could not connect to the DB server (host=$sHost, user=$sUser): ".mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno().')');
}
}
protected function QueryToScalar($sSql)
{
$result = mysqli_query($this->hDBLink, $sSql);
if (!$result)
{
throw new Exception("Failed to issue MySQL query '".$sSql."': ".mysqli_error($this->hDBLink).' (mysql errno: '.mysqli_errno($this->hDBLink).')');
}
if ($aRow = mysqli_fetch_array($result, MYSQLI_BOTH))
{
$res = $aRow[0];
}
else
{
mysqli_free_result($result);
throw new Exception("No result for query '".$sSql."'");
}
mysqli_free_result($result);
return $res;
}
}

View File

@@ -17,6 +17,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* OQL syntax analyzer, to be used prior to run the lexical analyzer
*
@@ -119,6 +120,11 @@ class OQLLexerRaw
'/\G-/ ',
'/\GAND/ ',
'/\GOR/ ',
'/\G\\|/ ',
'/\G&/ ',
'/\G\\^/ ',
'/\G<</ ',
'/\G>>/ ',
'/\G,/ ',
'/\G\\(/ ',
'/\G\\)/ ',
@@ -168,7 +174,8 @@ class OQLLexerRaw
'/\GABOVE STRICT/ ',
'/\GNOT ABOVE/ ',
'/\GNOT ABOVE STRICT/ ',
'/\G(0x[0-9a-fA-F]+|[0-9]+)/ ',
'/\G(0x[0-9a-fA-F]+)/ ',
'/\G([0-9]+)/ ',
'/\G\"([^\\\\\"]|\\\\\"|\\\\\\\\)*\"|'.chr(94).chr(39).'([^\\\\'.chr(39).']|\\\\'.chr(39).'|\\\\\\\\)*'.chr(39).'/ ',
'/\G([_a-zA-Z][_a-zA-Z0-9]*|`[^`]+`)/ ',
'/\G:([_a-zA-Z][_a-zA-Z0-9]*->[_a-zA-Z][_a-zA-Z0-9]*|[_a-zA-Z][_a-zA-Z0-9]*)/ ',
@@ -333,269 +340,299 @@ class OQLLexerRaw
function yy_r1_13($yy_subpatterns)
{
$this->token = OQLParser::COMA;
$this->token = OQLParser::BITWISE_OR;
}
function yy_r1_14($yy_subpatterns)
{
$this->token = OQLParser::PAR_OPEN;
$this->token = OQLParser::BITWISE_AND;
}
function yy_r1_15($yy_subpatterns)
{
$this->token = OQLParser::PAR_CLOSE;
$this->token = OQLParser::BITWISE_XOR;
}
function yy_r1_16($yy_subpatterns)
{
$this->token = OQLParser::REGEXP;
$this->token = OQLParser::BITWISE_LEFT_SHIFT;
}
function yy_r1_17($yy_subpatterns)
{
$this->token = OQLParser::EQ;
$this->token = OQLParser::BITWISE_RIGHT_SHIFT;
}
function yy_r1_18($yy_subpatterns)
{
$this->token = OQLParser::NOT_EQ;
$this->token = OQLParser::COMA;
}
function yy_r1_19($yy_subpatterns)
{
$this->token = OQLParser::GT;
$this->token = OQLParser::PAR_OPEN;
}
function yy_r1_20($yy_subpatterns)
{
$this->token = OQLParser::LT;
$this->token = OQLParser::PAR_CLOSE;
}
function yy_r1_21($yy_subpatterns)
{
$this->token = OQLParser::GE;
$this->token = OQLParser::REGEXP;
}
function yy_r1_22($yy_subpatterns)
{
$this->token = OQLParser::LE;
$this->token = OQLParser::EQ;
}
function yy_r1_23($yy_subpatterns)
{
$this->token = OQLParser::LIKE;
$this->token = OQLParser::NOT_EQ;
}
function yy_r1_24($yy_subpatterns)
{
$this->token = OQLParser::NOT_LIKE;
$this->token = OQLParser::GT;
}
function yy_r1_25($yy_subpatterns)
{
$this->token = OQLParser::IN;
$this->token = OQLParser::LT;
}
function yy_r1_26($yy_subpatterns)
{
$this->token = OQLParser::NOT_IN;
$this->token = OQLParser::GE;
}
function yy_r1_27($yy_subpatterns)
{
$this->token = OQLParser::INTERVAL;
$this->token = OQLParser::LE;
}
function yy_r1_28($yy_subpatterns)
{
$this->token = OQLParser::F_IF;
$this->token = OQLParser::LIKE;
}
function yy_r1_29($yy_subpatterns)
{
$this->token = OQLParser::F_ELT;
$this->token = OQLParser::NOT_LIKE;
}
function yy_r1_30($yy_subpatterns)
{
$this->token = OQLParser::F_COALESCE;
$this->token = OQLParser::IN;
}
function yy_r1_31($yy_subpatterns)
{
$this->token = OQLParser::F_ISNULL;
$this->token = OQLParser::NOT_IN;
}
function yy_r1_32($yy_subpatterns)
{
$this->token = OQLParser::F_CONCAT;
$this->token = OQLParser::INTERVAL;
}
function yy_r1_33($yy_subpatterns)
{
$this->token = OQLParser::F_SUBSTR;
$this->token = OQLParser::F_IF;
}
function yy_r1_34($yy_subpatterns)
{
$this->token = OQLParser::F_TRIM;
$this->token = OQLParser::F_ELT;
}
function yy_r1_35($yy_subpatterns)
{
$this->token = OQLParser::F_DATE;
$this->token = OQLParser::F_COALESCE;
}
function yy_r1_36($yy_subpatterns)
{
$this->token = OQLParser::F_DATE_FORMAT;
$this->token = OQLParser::F_ISNULL;
}
function yy_r1_37($yy_subpatterns)
{
$this->token = OQLParser::F_CURRENT_DATE;
$this->token = OQLParser::F_CONCAT;
}
function yy_r1_38($yy_subpatterns)
{
$this->token = OQLParser::F_NOW;
$this->token = OQLParser::F_SUBSTR;
}
function yy_r1_39($yy_subpatterns)
{
$this->token = OQLParser::F_TIME;
$this->token = OQLParser::F_TRIM;
}
function yy_r1_40($yy_subpatterns)
{
$this->token = OQLParser::F_TO_DAYS;
$this->token = OQLParser::F_DATE;
}
function yy_r1_41($yy_subpatterns)
{
$this->token = OQLParser::F_FROM_DAYS;
$this->token = OQLParser::F_DATE_FORMAT;
}
function yy_r1_42($yy_subpatterns)
{
$this->token = OQLParser::F_YEAR;
$this->token = OQLParser::F_CURRENT_DATE;
}
function yy_r1_43($yy_subpatterns)
{
$this->token = OQLParser::F_MONTH;
$this->token = OQLParser::F_NOW;
}
function yy_r1_44($yy_subpatterns)
{
$this->token = OQLParser::F_DAY;
$this->token = OQLParser::F_TIME;
}
function yy_r1_45($yy_subpatterns)
{
$this->token = OQLParser::F_HOUR;
$this->token = OQLParser::F_TO_DAYS;
}
function yy_r1_46($yy_subpatterns)
{
$this->token = OQLParser::F_MINUTE;
$this->token = OQLParser::F_FROM_DAYS;
}
function yy_r1_47($yy_subpatterns)
{
$this->token = OQLParser::F_SECOND;
$this->token = OQLParser::F_YEAR;
}
function yy_r1_48($yy_subpatterns)
{
$this->token = OQLParser::F_DATE_ADD;
$this->token = OQLParser::F_MONTH;
}
function yy_r1_49($yy_subpatterns)
{
$this->token = OQLParser::F_DATE_SUB;
$this->token = OQLParser::F_DAY;
}
function yy_r1_50($yy_subpatterns)
{
$this->token = OQLParser::F_ROUND;
$this->token = OQLParser::F_HOUR;
}
function yy_r1_51($yy_subpatterns)
{
$this->token = OQLParser::F_FLOOR;
$this->token = OQLParser::F_MINUTE;
}
function yy_r1_52($yy_subpatterns)
{
$this->token = OQLParser::F_INET_ATON;
$this->token = OQLParser::F_SECOND;
}
function yy_r1_53($yy_subpatterns)
{
$this->token = OQLParser::F_INET_NTOA;
$this->token = OQLParser::F_DATE_ADD;
}
function yy_r1_54($yy_subpatterns)
{
$this->token = OQLParser::BELOW;
$this->token = OQLParser::F_DATE_SUB;
}
function yy_r1_55($yy_subpatterns)
{
$this->token = OQLParser::BELOW_STRICT;
$this->token = OQLParser::F_ROUND;
}
function yy_r1_56($yy_subpatterns)
{
$this->token = OQLParser::NOT_BELOW;
$this->token = OQLParser::F_FLOOR;
}
function yy_r1_57($yy_subpatterns)
{
$this->token = OQLParser::NOT_BELOW_STRICT;
$this->token = OQLParser::F_INET_ATON;
}
function yy_r1_58($yy_subpatterns)
{
$this->token = OQLParser::ABOVE;
$this->token = OQLParser::F_INET_NTOA;
}
function yy_r1_59($yy_subpatterns)
{
$this->token = OQLParser::ABOVE_STRICT;
$this->token = OQLParser::BELOW;
}
function yy_r1_60($yy_subpatterns)
{
$this->token = OQLParser::NOT_ABOVE;
$this->token = OQLParser::BELOW_STRICT;
}
function yy_r1_61($yy_subpatterns)
{
$this->token = OQLParser::NOT_ABOVE_STRICT;
$this->token = OQLParser::NOT_BELOW;
}
function yy_r1_62($yy_subpatterns)
{
$this->token = OQLParser::NUMVAL;
$this->token = OQLParser::NOT_BELOW_STRICT;
}
function yy_r1_63($yy_subpatterns)
{
$this->token = OQLParser::STRVAL;
$this->token = OQLParser::ABOVE;
}
function yy_r1_64($yy_subpatterns)
{
$this->token = OQLParser::NAME;
$this->token = OQLParser::ABOVE_STRICT;
}
function yy_r1_65($yy_subpatterns)
{
$this->token = OQLParser::VARNAME;
$this->token = OQLParser::NOT_ABOVE;
}
function yy_r1_66($yy_subpatterns)
{
$this->token = OQLParser::NOT_ABOVE_STRICT;
}
function yy_r1_67($yy_subpatterns)
{
$this->token = OQLParser::HEXVAL;
}
function yy_r1_68($yy_subpatterns)
{
$this->token = OQLParser::NUMVAL;
}
function yy_r1_69($yy_subpatterns)
{
$this->token = OQLParser::STRVAL;
}
function yy_r1_70($yy_subpatterns)
{
$this->token = OQLParser::NAME;
}
function yy_r1_71($yy_subpatterns)
{
$this->token = OQLParser::VARNAME;
}
function yy_r1_72($yy_subpatterns)
{
$this->token = OQLParser::DOT;

View File

@@ -95,6 +95,11 @@ math_plus = "+"
math_minus = "-"
log_and = "AND"
log_or = "OR"
bitwise_and = "&"
bitwise_or = "|"
bitwise_xor = "^"
bitwise_leftshift = "<<"
bitwise_rightshift = ">>"
regexp = "REGEXP"
eq = "="
not_eq = "!="
@@ -156,8 +161,11 @@ not_above_strict = "NOT ABOVE STRICT"
//
// numval = /([0-9]+|0x[0-9a-fA-F]+)/
// Does not work either, the hexadecimal numbers are not matched properly
// The following seems to work...
numval = /(0x[0-9a-fA-F]+|[0-9]+)/
// Anyhow let's distinguish the hexadecimal values from decimal integers, hex numbers will be stored as strings
// and passed as-is to MySQL which enables us to pass 64-bit values without messing with them in PHP
//
hexval = /(0x[0-9a-fA-F]+)/
numval = /([0-9]+)/
strval = /"([^\\"]|\\"|\\\\)*"|'.chr(94).chr(39).'([^\\'.chr(39).']|\\'.chr(39).'|\\\\)*'.chr(39).'/
name = /([_a-zA-Z][_a-zA-Z0-9]*|`[^`]+`)/
varname = /:([_a-zA-Z][_a-zA-Z0-9]*->[_a-zA-Z][_a-zA-Z0-9]*|[_a-zA-Z][_a-zA-Z0-9]*)/
@@ -204,6 +212,21 @@ log_and {
log_or {
$this->token = OQLParser::LOG_OR;
}
bitwise_or {
$this->token = OQLParser::BITWISE_OR;
}
bitwise_and {
$this->token = OQLParser::BITWISE_AND;
}
bitwise_xor {
$this->token = OQLParser::BITWISE_XOR;
}
bitwise_leftshift {
$this->token = OQLParser::BITWISE_LEFT_SHIFT;
}
bitwise_rightshift {
$this->token = OQLParser::BITWISE_RIGHT_SHIFT;
}
coma {
$this->token = OQLParser::COMA;
}
@@ -351,6 +374,9 @@ not_above {
not_above_strict {
$this->token = OQLParser::NOT_ABOVE_STRICT;
}
hexval {
$this->token = OQLParser::HEXVAL;
}
numval {
$this->token = OQLParser::NUMVAL;
}

File diff suppressed because it is too large Load Diff

View File

@@ -164,19 +164,23 @@ name(A) ::= NAME(X). {
}
A = new OqlName($name, $this->m_iColPrev);
}
num_value(A) ::= NUMVAL(X). {A=X;}
num_value(A) ::= NUMVAL(X). {A=(int)X;}
num_value(A) ::= MATH_MINUS NUMVAL(X). {A=(int)-X;}
num_value(A) ::= HEXVAL(X). {A=new OqlHexValue(X);}
str_value(A) ::= STRVAL(X). {A=stripslashes(substr(X, 1, strlen(X) - 2));}
operator1(A) ::= num_operator1(X). {A=X;}
operator1(A) ::= bitwise_operator1(X). {A=X;}
operator2(A) ::= num_operator2(X). {A=X;}
operator2(A) ::= str_operator(X). {A=X;}
operator2(A) ::= REGEXP(X). {A=X;}
operator2(A) ::= EQ(X). {A=X;}
operator2(A) ::= NOT_EQ(X). {A=X;}
operator3(A) ::= LOG_AND(X). {A=X;}
operator3(A) ::= bitwise_operator3(X). {A=X;}
operator4(A) ::= LOG_OR(X). {A=X;}
operator4(A) ::= bitwise_operator4(X). {A=X;}
num_operator1(A) ::= MATH_DIV(X). {A=X;}
num_operator1(A) ::= MATH_MULT(X). {A=X;}
@@ -190,6 +194,12 @@ num_operator2(A) ::= LE(X). {A=X;}
str_operator(A) ::= LIKE(X). {A=X;}
str_operator(A) ::= NOT_LIKE(X). {A=X;}
bitwise_operator1(A) ::= BITWISE_LEFT_SHIFT(X). {A=X;}
bitwise_operator1(A) ::= BITWISE_RIGHT_SHIFT(X). {A=X;}
bitwise_operator3(A) ::= BITWISE_AND(X). {A=X;}
bitwise_operator4(A) ::= BITWISE_OR(X). {A=X;}
bitwise_operator4(A) ::= BITWISE_XOR(X). {A=X;}
list_operator(A) ::= IN(X). {A=X;}
list_operator(A) ::= NOT_IN(X). {A=X;}

View File

@@ -65,8 +65,11 @@ class OQLException extends CoreException
if (!is_null($this->m_aExpecting) && (count($this->m_aExpecting) > 0))
{
$sExpectations = '{'.implode(', ', $this->m_aExpecting).'}';
$sRet .= ", expecting ".htmlentities($sExpectations, ENT_QUOTES, 'UTF-8');
if (count($this->m_aExpecting) < 30)
{
$sExpectations = '{'.implode(', ', $this->m_aExpecting).'}';
$sRet .= ", expecting ".htmlentities($sExpectations, ENT_QUOTES, 'UTF-8');
}
$sSuggest = self::FindClosestString($this->m_sUnexpected, $this->m_aExpecting);
if (strlen($sSuggest) > 0)
{

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -54,6 +54,27 @@ class OqlName
}
}
/**
*
* Store hexadecimal values as strings so that we can support 64-bit values
*
*/
class OqlHexValue
{
protected $m_sValue;
public function __construct($sValue)
{
$this->m_sValue = $sValue;
}
public function __toString()
{
return $this->m_sValue;
}
}
class OqlJoinSpec
{
protected $m_oClass;
@@ -106,15 +127,38 @@ class OqlJoinSpec
}
}
class BinaryOqlExpression extends BinaryExpression
interface CheckableExpression
{
/**
* Check the validity of the expression with regard to the data model
* and the query in which it is used
*
* @param ModelReflection $oModelReflection MetaModel to consider
* @param array $aAliases Aliases to class names (for the current query)
* @param string $sSourceQuery For the reporting
* @throws OqlNormalizeException
*/
public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery);
}
class ScalarOqlExpression extends ScalarExpression
class BinaryOqlExpression extends BinaryExpression implements CheckableExpression
{
public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
{
$this->m_oLeftExpr->Check($oModelReflection, $aAliases, $sSourceQuery);
$this->m_oRightExpr->Check($oModelReflection, $aAliases, $sSourceQuery);
}
}
class FieldOqlExpression extends FieldExpression
class ScalarOqlExpression extends ScalarExpression implements CheckableExpression
{
public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
{
// a scalar is always fine
}
}
class FieldOqlExpression extends FieldExpression implements CheckableExpression
{
protected $m_oParent;
protected $m_oName;
@@ -140,22 +184,84 @@ class FieldOqlExpression extends FieldExpression
{
return $this->m_oName;
}
public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
{
$sClassAlias = $this->GetParent();
$sFltCode = $this->GetName();
if (empty($sClassAlias))
{
// Try to find an alias
// Build an array of field => array of aliases
$aFieldClasses = array();
foreach($aAliases as $sAlias => $sReal)
{
foreach($oModelReflection->GetFiltersList($sReal) as $sAnFltCode)
{
$aFieldClasses[$sAnFltCode][] = $sAlias;
}
}
if (!array_key_exists($sFltCode, $aFieldClasses))
{
throw new OqlNormalizeException('Unknown filter code', $sSourceQuery, $this->GetNameDetails(), array_keys($aFieldClasses));
}
if (count($aFieldClasses[$sFltCode]) > 1)
{
throw new OqlNormalizeException('Ambiguous filter code', $sSourceQuery, $this->GetNameDetails());
}
$sClassAlias = $aFieldClasses[$sFltCode][0];
}
else
{
if (!array_key_exists($sClassAlias, $aAliases))
{
throw new OqlNormalizeException('Unknown class [alias]', $sSourceQuery, $this->GetParentDetails(), array_keys($aAliases));
}
$sClass = $aAliases[$sClassAlias];
if (!$oModelReflection->IsValidFilterCode($sClass, $sFltCode))
{
throw new OqlNormalizeException('Unknown filter code', $sSourceQuery, $this->GetNameDetails(), $oModelReflection->GetFiltersList($sClass));
}
}
}
}
class VariableOqlExpression extends VariableExpression
class VariableOqlExpression extends VariableExpression implements CheckableExpression
{
public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
{
// a scalar is always fine
}
}
class ListOqlExpression extends ListExpression
class ListOqlExpression extends ListExpression implements CheckableExpression
{
public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
{
foreach ($this->GetItems() as $oItemExpression)
{
$oItemExpression->Check($oModelReflection, $aAliases, $sSourceQuery);
}
}
}
class FunctionOqlExpression extends FunctionExpression
class FunctionOqlExpression extends FunctionExpression implements CheckableExpression
{
public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
{
foreach ($this->GetArgs() as $oArgExpression)
{
$oArgExpression->Check($oModelReflection, $aAliases, $sSourceQuery);
}
}
}
class IntervalOqlExpression extends IntervalExpression
class IntervalOqlExpression extends IntervalExpression implements CheckableExpression
{
public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
{
// an interval is always fine (made of a scalar and unit)
}
}
abstract class OqlQuery
@@ -214,6 +320,155 @@ class OqlObjectQuery extends OqlQuery
{
return $this->m_oClassAlias;
}
/**
* Recursively check the validity of the expression with regard to the data model
* and the query in which it is used
*
* @param ModelReflection $oModelReflection MetaModel to consider
* @throws OqlNormalizeException
*/
public function Check(ModelReflection $oModelReflection, $sSourceQuery)
{
$sClass = $this->GetClass();
$sClassAlias = $this->GetClassAlias();
if (!$oModelReflection->IsValidClass($sClass))
{
throw new UnknownClassOqlException($sSourceQuery, $this->GetClassDetails(), $oModelReflection->GetClasses());
}
$aAliases = array($sClassAlias => $sClass);
$aJoinSpecs = $this->GetJoins();
if (is_array($aJoinSpecs))
{
foreach ($aJoinSpecs as $oJoinSpec)
{
$sJoinClass = $oJoinSpec->GetClass();
$sJoinClassAlias = $oJoinSpec->GetClassAlias();
if (!$oModelReflection->IsValidClass($sJoinClass))
{
throw new UnknownClassOqlException($sSourceQuery, $oJoinSpec->GetClassDetails(), $oModelReflection->GetClasses());
}
if (array_key_exists($sJoinClassAlias, $aAliases))
{
if ($sJoinClassAlias != $sJoinClass)
{
throw new OqlNormalizeException('Duplicate class alias', $sSourceQuery, $oJoinSpec->GetClassAliasDetails());
}
else
{
throw new OqlNormalizeException('Duplicate class name', $sSourceQuery, $oJoinSpec->GetClassDetails());
}
}
// Assumption: ext key on the left only !!!
// normalization should take care of this
$oLeftField = $oJoinSpec->GetLeftField();
$sFromClass = $oLeftField->GetParent();
$sExtKeyAttCode = $oLeftField->GetName();
$oRightField = $oJoinSpec->GetRightField();
$sToClass = $oRightField->GetParent();
$sPKeyDescriptor = $oRightField->GetName();
if ($sPKeyDescriptor != 'id')
{
throw new OqlNormalizeException('Wrong format for Join clause (right hand), expecting an id', $sSourceQuery, $oRightField->GetNameDetails(), array('id'));
}
$aAliases[$sJoinClassAlias] = $sJoinClass;
if (!array_key_exists($sFromClass, $aAliases))
{
throw new OqlNormalizeException('Unknown class in join condition (left expression)', $sSourceQuery, $oLeftField->GetParentDetails(), array_keys($aAliases));
}
if (!array_key_exists($sToClass, $aAliases))
{
throw new OqlNormalizeException('Unknown class in join condition (right expression)', $sSourceQuery, $oRightField->GetParentDetails(), array_keys($aAliases));
}
$aExtKeys = $oModelReflection->ListAttributes($aAliases[$sFromClass], 'AttributeExternalKey');
if (!array_key_exists($sExtKeyAttCode, $aExtKeys))
{
throw new OqlNormalizeException('Unknown external key in join condition (left expression)', $sSourceQuery, $oLeftField->GetNameDetails(), array_keys($aExtKeys));
}
if ($sFromClass == $sJoinClassAlias)
{
$sTargetClass = $oModelReflection->GetAttributeProperty($aAliases[$sFromClass], $sExtKeyAttCode, 'targetclass');
if(!$oModelReflection->IsSameFamilyBranch($aAliases[$sToClass], $sTargetClass))
{
throw new OqlNormalizeException("The joined class ($aAliases[$sFromClass]) is not compatible with the external key, which is pointing to $sTargetClass", $sSourceQuery, $oLeftField->GetNameDetails());
}
}
else
{
$sOperator = $oJoinSpec->GetOperator();
switch($sOperator)
{
case '=':
$iOperatorCode = TREE_OPERATOR_EQUALS;
break;
case 'BELOW':
$iOperatorCode = TREE_OPERATOR_BELOW;
break;
case 'BELOW_STRICT':
$iOperatorCode = TREE_OPERATOR_BELOW_STRICT;
break;
case 'NOT_BELOW':
$iOperatorCode = TREE_OPERATOR_NOT_BELOW;
break;
case 'NOT_BELOW_STRICT':
$iOperatorCode = TREE_OPERATOR_NOT_BELOW_STRICT;
break;
case 'ABOVE':
$iOperatorCode = TREE_OPERATOR_ABOVE;
break;
case 'ABOVE_STRICT':
$iOperatorCode = TREE_OPERATOR_ABOVE_STRICT;
break;
case 'NOT_ABOVE':
$iOperatorCode = TREE_OPERATOR_NOT_ABOVE;
break;
case 'NOT_ABOVE_STRICT':
$iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT;
break;
}
$sTargetClass = $oModelReflection->GetAttributeProperty($aAliases[$sFromClass], $sExtKeyAttCode, 'targetclass');
if(!$oModelReflection->IsSameFamilyBranch($aAliases[$sToClass], $sTargetClass))
{
throw new OqlNormalizeException("The joined class ($aAliases[$sToClass]) is not compatible with the external key, which is pointing to $sTargetClass", $sSourceQuery, $oLeftField->GetNameDetails());
}
$aAttList = $oModelReflection->ListAttributes($aAliases[$sFromClass]);
$sAttType = $aAttList[$sExtKeyAttCode];
if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !is_subclass_of($sAttType, 'AttributeHierarchicalKey') && ($sAttType != 'AttributeHierarchicalKey'))
{
throw new OqlNormalizeException("The specified tree operator $sOperator is not applicable to the key", $sSourceQuery, $oLeftField->GetNameDetails());
}
}
}
}
// Check the select information
//
$aSelected = array();
foreach ($this->GetSelectedClasses() as $oClassDetails)
{
$sClassToSelect = $oClassDetails->GetValue();
if (!array_key_exists($sClassToSelect, $aAliases))
{
throw new OqlNormalizeException('Unknown class [alias]', $sSourceQuery, $oClassDetails, array_keys($aAliases));
}
$aSelected[$sClassToSelect] = $aAliases[$sClassToSelect];
}
// Check the condition tree
//
if ($this->m_oCondition instanceof Expression)
{
$this->m_oCondition->Check($oModelReflection, $aAliases, $sSourceQuery);
}
}
}
?>

View File

@@ -48,6 +48,80 @@ class ormCaseLog {
return $this->m_sLog;
}
public static function FromJSON($oJson)
{
if (!isset($oJson->items))
{
throw new Exception("Missing 'items' elements");
}
$oCaseLog = new ormCaseLog();
foreach($oJson->items as $oItem)
{
$oCaseLog->AddLogEntryFromJSON($oItem);
}
return $oCaseLog;
}
/**
* Return a value that will be further JSON encoded
*/
public function GetForJSON()
{
$aEntries = array();
$iPos = 0;
for($index=count($this->m_aIndex)-1 ; $index >= 0 ; $index--)
{
$iPos += $this->m_aIndex[$index]['separator_length'];
$sTextEntry = substr($this->m_sLog, $iPos, $this->m_aIndex[$index]['text_length']);
$iPos += $this->m_aIndex[$index]['text_length'];
// Workaround: PHP < 5.3 cannot unserialize correctly DateTime objects,
// therefore we have changed the format. To preserve the compatibility with existing
// installations of iTop, both format are allowed:
// the 'date' item is either a DateTime object, or a unix timestamp
if (is_int($this->m_aIndex[$index]['date']))
{
// Unix timestamp
$sDate = date(Dict::S('UI:CaseLog:DateFormat'),$this->m_aIndex[$index]['date']);
}
elseif (is_object($this->m_aIndex[$index]['date']))
{
if (version_compare(phpversion(), '5.3.0', '>='))
{
// DateTime
$sDate = $this->m_aIndex[$index]['date']->format(Dict::S('UI:CaseLog:DateFormat'));
}
else
{
// No Warning... but the date is unknown
$sDate = '';
}
}
$aEntries[] = array(
'date' => $sDate,
'user_login' => $this->m_aIndex[$index]['user_name'],
'user_id' => $this->m_aIndex[$index]['user_id'],
'message' => $sTextEntry
);
}
// Process the case of an eventual remainder (quick migration of AttributeText fields)
if ($iPos < (strlen($this->m_sLog) - 1))
{
$sTextEntry = substr($this->m_sLog, $iPos);
$aEntries[] = array(
'date' => '',
'user_login' => '',
'message' => $sTextEntry
);
}
// Order by ascending date
$aRet = array('entries' => array_reverse($aEntries));
return $aRet;
}
public function GetIndex()
{
return $this->m_aIndex;
@@ -62,7 +136,85 @@ class ormCaseLog {
{
$this->m_bModified = false;
}
/**
* Produces an HTML representation, aimed at being used within an email
*/
public function GetAsEmailHtml()
{
$sStyleCaseLogHeader = '';
$sStyleCaseLogEntry = '';
$sHtml = '<table style="width:100%;table-layout:fixed"><tr><td>'; // Use table-layout:fixed to force the with to be independent from the actual content
$iPos = 0;
$aIndex = $this->m_aIndex;
for($index=count($aIndex)-1 ; $index >= 0 ; $index--)
{
$iPos += $aIndex[$index]['separator_length'];
$sTextEntry = substr($this->m_sLog, $iPos, $aIndex[$index]['text_length']);
$sTextEntry = str_replace(array("\r\n", "\n", "\r"), "<br/>", htmlentities($sTextEntry, ENT_QUOTES, 'UTF-8'));
$iPos += $aIndex[$index]['text_length'];
$sEntry = '<div class="caselog_header" style="'.$sStyleCaseLogHeader.'">';
// Workaround: PHP < 5.3 cannot unserialize correctly DateTime objects,
// therefore we have changed the format. To preserve the compatibility with existing
// installations of iTop, both format are allowed:
// the 'date' item is either a DateTime object, or a unix timestamp
if (is_int($aIndex[$index]['date']))
{
// Unix timestamp
$sDate = date(Dict::S('UI:CaseLog:DateFormat'),$aIndex[$index]['date']);
}
elseif (is_object($aIndex[$index]['date']))
{
if (version_compare(phpversion(), '5.3.0', '>='))
{
// DateTime
$sDate = $aIndex[$index]['date']->format(Dict::S('UI:CaseLog:DateFormat'));
}
else
{
// No Warning... but the date is unknown
$sDate = '';
}
}
$sEntry .= sprintf(Dict::S('UI:CaseLog:Header_Date_UserName'), '<span class="caselog_header_date">'.$sDate.'</span>', '<span class="caselog_header_user">'.$aIndex[$index]['user_name'].'</span>');
$sEntry .= '</div>';
$sEntry .= '<div class="caselog_entry" style="'.$sStyleCaseLogEntry.'">';
$sEntry .= $sTextEntry;
$sEntry .= '</div>';
$sHtml = $sHtml.$sEntry;
}
// Process the case of an eventual remainder (quick migration of AttributeText fields)
if ($iPos < (strlen($this->m_sLog) - 1))
{
$sTextEntry = substr($this->m_sLog, $iPos);
$sTextEntry = str_replace(array("\r\n", "\n", "\r"), "<br/>", htmlentities($sTextEntry, ENT_QUOTES, 'UTF-8'));
if (count($this->m_aIndex) == 0)
{
$sHtml .= '<div class="caselog_entry" style="'.$sStyleCaseLogEntry.'"">';
$sHtml .= $sTextEntry;
$sHtml .= '</div>';
}
else
{
$sHtml .= '<div class="caselog_header" style="'.$sStyleCaseLogHeader.'">';
$sHtml .= Dict::S('UI:CaseLog:InitialValue');
$sHtml .= '</div>';
$sHtml .= '<div class="caselog_entry" style="'.$sStyleCaseLogEntry.'">';
$sHtml .= $sTextEntry;
$sHtml .= '</div>';
}
}
$sHtml .= '</td></tr></table>';
return $sHtml;
}
/**
* Produces an HTML representation, aimed at being used within the iTop framework
*/
public function GetAsHTML(WebPage $oP = null, $bEditMode = false, $aTransfoHandler = null)
{
$sHtml = '<table style="width:100%;table-layout:fixed"><tr><td>'; // Use table-layout:fixed to force the with to be independent from the actual content
@@ -230,7 +382,62 @@ class ormCaseLog {
}
$this->m_bModified = true;
}
public function AddLogEntryFromJSON($oJson)
{
$sText = isset($oJson->message) ? $oJson->message : '';
if (isset($oJson->user_id))
{
if (!UserRights::IsAdministrator())
{
throw new Exception("Only administrators can set the user id", RestResult::UNAUTHORIZED);
}
try
{
$oUser = RestUtils::FindObjectFromKey('User', $oJson->user_id);
}
catch(Exception $e)
{
throw new Exception('user_id: '.$e->getMessage(), $e->getCode());
}
$iUserId = $oUser->GetKey();
$sOnBehalfOf = $oUser->GetFriendlyName();
}
else
{
$iUserId = UserRights::GetUserId();
$sOnBehalfOf = UserRights::GetUserFriendlyName();
}
if (isset($oJson->date))
{
$oDate = new DateTime($oJson->date);
$iDate = (int) $oDate->format('U');
}
else
{
$iDate = time();
}
$sDate = date(Dict::S('UI:CaseLog:DateFormat'), $iDate);
$sSeparator = sprintf(CASELOG_SEPARATOR, $sDate, $sOnBehalfOf, $iUserId);
$iSepLength = strlen($sSeparator);
$iTextlength = strlen($sText);
$this->m_sLog = $sSeparator.$sText.$this->m_sLog; // Latest entry printed first
$this->m_aIndex[] = array(
'user_name' => $sOnBehalfOf,
'user_id' => $iUserId,
'date' => $iDate,
'text_length' => $iTextlength,
'separator_length' => $iSepLength,
);
$this->m_bModified = true;
}
public function GetModifiedEntry()
{
$sModifiedEntry = '';

View File

@@ -117,5 +117,32 @@ class ormDocument
{
return "<a href=\"".utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php?operation=download_document&class=$sClass&id=$Id&field=$sAttCode\">".$this->GetFileName()."</a>\n";
}
/**
* Returns an URL to download a document like an image (uses HTTP caching)
* @return string
*/
public function GetDownloadURL($sClass, $Id, $sAttCode)
{
// Compute a signature to reset the cache anytime the data changes (this is acceptable if used only with icon files)
$sSignature = md5($this->GetData());
return utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php?operation=download_document&class=$sClass&id=$Id&field=$sAttCode&s=$sSignature&cache=86400";
}
public function IsPreviewAvailable()
{
$bRet = false;
switch($this->GetMimeType())
{
case 'image/png':
case 'image/jpg':
case 'image/jpeg':
case 'image/gif':
$bRet = true;
break;
}
return $bRet;
}
}
?>

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -66,7 +66,6 @@ class ormStopWatch
{
$this->aThresholds[$iPercent] = array(
'deadline' => $tDeadline, // unix time (seconds)
'passed' => $bPassed,
'triggered' => $bTriggered,
'overrun' => $iOverrun
);
@@ -122,14 +121,16 @@ class ormStopWatch
}
public function IsThresholdPassed($iPercent)
{
$bRet = false;
if (array_key_exists($iPercent, $this->aThresholds))
{
return $this->aThresholds[$iPercent]['passed'];
}
else
{
return false;
$aThresholdData = $this->aThresholds[$iPercent];
if (!is_null($aThresholdData['deadline']) && ($aThresholdData['deadline'] <= time()))
{
$bRet = true;
}
}
return $bRet;
}
public function IsThresholdTriggered($iPercent)
{
@@ -162,8 +163,7 @@ class ormStopWatch
}
else
{
$iElapsedTemp = ''; //$this->ComputeDuration($oHostObject, $oAttDef, $this->iLastStart, time());
$aProperties['Elapsed'] = $this->iTimeSpent.' + '.$iElapsedTemp.' s + <img src="../images/indicator.gif">';
$aProperties['Elapsed'] = 'running <img src="../images/indicator.gif">';
}
$aProperties['Started'] = $oAttDef->SecondsToDate($this->iStarted);
@@ -183,7 +183,7 @@ class ormStopWatch
}
$aProperties[$iPercent.'%'] = $sThresholdDesc;
}
$sRes = "<TABLE class=\"listResults\">";
$sRes = "<TABLE>";
$sRes .= "<TBODY>";
foreach ($aProperties as $sProperty => $sValue)
{
@@ -213,6 +213,10 @@ class ormStopWatch
protected function ComputeDeadline($oObject, $oAttDef, $iStartTime, $iDurationSec)
{
$sWorkingTimeComputer = $oAttDef->Get('working_time_computing');
if ($sWorkingTimeComputer == '')
{
$sWorkingTimeComputer = class_exists('SLAComputation') ? 'SLAComputation' : 'DefaultWorkingTimeComputer';
}
$aCallSpec = array($sWorkingTimeComputer, '__construct');
if (!is_callable($aCallSpec))
{
@@ -234,6 +238,10 @@ class ormStopWatch
protected function ComputeDuration($oObject, $oAttDef, $iStartTime, $iEndTime)
{
$sWorkingTimeComputer = $oAttDef->Get('working_time_computing');
if ($sWorkingTimeComputer == '')
{
$sWorkingTimeComputer = class_exists('SLAComputation') ? 'SLAComputation' : 'DefaultWorkingTimeComputer';
}
$oComputer = new $sWorkingTimeComputer();
$aCallSpec = array($oComputer, 'GetOpenDuration');
if (!is_callable($aCallSpec))
@@ -250,17 +258,22 @@ class ormStopWatch
public function Reset($oObject, $oAttDef)
{
$this->iTimeSpent = 0;
$this->iStarted = null;
$this->iLastStart = null;
$this->iStopped = null;
$this->iStarted = null;
foreach ($this->aThresholds as $iPercent => &$aThresholdData)
{
$aThresholdData['passed'] = false;
$aThresholdData['triggered'] = false;
$aThresholdData['deadline'] = null;
$aThresholdData['overrun'] = null;
}
if (!is_null($this->iLastStart))
{
// Currently running... starting again from now!
$this->iStarted = time();
$this->iLastStart = time();
$this->ComputeDeadlines($oObject, $oAttDef);
}
}
/**
@@ -315,14 +328,12 @@ class ormStopWatch
if (is_null($aThresholdData['deadline']) || ($aThresholdData['deadline'] > time()))
{
// The threshold is in the future, reset
$aThresholdData['passed'] = false;
$aThresholdData['triggered'] = false;
$aThresholdData['overrun'] = null;
}
else
{
// The new threshold is in the past
$aThresholdData['passed'] = true;
// Note: the overrun can be wrong, but the correct algorithm to compute
// the overrun of a deadline in the past requires that the ormStopWatch keeps track of all its history!!!
}
@@ -360,7 +371,6 @@ class ormStopWatch
$iOverrun = $this->ComputeDuration($oObject, $oAttDef, $aThresholdData['deadline'], time());
$aThresholdData['overrun'] = $iOverrun;
}
$aThresholdData['passed'] = true;
}
$aThresholdData['deadline'] = null;
}
@@ -387,6 +397,7 @@ class CheckStopWatchThresholds implements iBackgroundProcess
public function Process($iTimeLimit)
{
$aList = array();
foreach (MetaModel::GetClasses() as $sClass)
{
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
@@ -397,17 +408,15 @@ class CheckStopWatchThresholds implements iBackgroundProcess
{
$iPercent = $aThresholdData['percent']; // could be different than the index !
$sExpression = "SELECT $sClass WHERE {$sAttCode}_laststart AND {$sAttCode}_{$iThreshold}_triggered = 0 AND {$sAttCode}_{$iThreshold}_deadline < NOW()";
//echo $sExpression."<br/>\n";
$sNow = date('Y-m-d H:i:s');
$sExpression = "SELECT $sClass WHERE {$sAttCode}_laststart AND {$sAttCode}_{$iThreshold}_triggered = 0 AND {$sAttCode}_{$iThreshold}_deadline < '$sNow'";
$oFilter = DBObjectSearch::FromOQL($sExpression);
$aList = array();
$oSet = new DBObjectSet($oFilter);
while ((time() < $iTimeLimit) && ($oObj = $oSet->Fetch()))
{
$sClass = get_class($oObj);
$aList[] = $sClass.'::'.$oObj->GetKey().' '.$sAttCode.' '.$iThreshold;
//echo $sClass.'::'.$oObj->GetKey().' '.$sAttCode.' '.$iThreshold."\n";
// Execute planned actions
//
@@ -416,7 +425,6 @@ class CheckStopWatchThresholds implements iBackgroundProcess
$sVerb = $aActionData['verb'];
$aParams = $aActionData['params'];
$sParams = implode(', ', $aParams);
//echo "Calling: $sVerb($sParams)<br/>\n";
$aCallSpec = array($oObj, $sVerb);
call_user_func_array($aCallSpec, $aParams);
}
@@ -438,12 +446,12 @@ class CheckStopWatchThresholds implements iBackgroundProcess
// Activate any existing trigger
//
$sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL));
$oSet = new DBObjectSet(
$oTriggerSet = new DBObjectSet(
DBObjectSearch::FromOQL("SELECT TriggerOnThresholdReached AS t WHERE t.target_class IN ('$sClassList') AND stop_watch_code=:stop_watch_code AND threshold_index = :threshold_index"),
array(), // order by
array('stop_watch_code' => $sAttCode, 'threshold_index' => $iThreshold)
);
while ($oTrigger = $oSet->Fetch())
while ($oTrigger = $oTriggerSet->Fetch())
{
$oTrigger->DoActivate($oObj->ToArgs('this'));
}
@@ -454,9 +462,6 @@ class CheckStopWatchThresholds implements iBackgroundProcess
}
$iProcessed = count($aList);
return "Triggered $iProcessed threshold(s)";
return "Triggered $iProcessed threshold(s):".implode(", ", $aList);
}
}
?>

View File

@@ -0,0 +1,643 @@
<?php
// Copyright (C) 2013 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/>
/**
* REST/json services
*
* Definition of common structures + the very minimum service provider (manage objects)
*
* @package REST Services
* @copyright Copyright (C) 2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
* @api
*/
/**
* Element of the response formed by RestResultWithObjects
*
* @package REST Services
*/
class ObjectResult
{
public $code;
public $message;
public $class;
public $key;
public $fields;
/**
* Default constructor
*/
public function __construct($sClass = null, $iId = null)
{
$this->code = RestResult::OK;
$this->message = '';
$this->class = $sClass;
$this->key = $iId;
$this->fields = array();
}
/**
* Helper to make an output value for a given attribute
*
* @param DBObject $oObject The object being reported
* @param string $sAttCode The attribute code (must be valid)
* @param boolean $bExtendedOutput Output all of the link set attributes ?
* @return string A scalar representation of the value
*/
protected function MakeResultValue(DBObject $oObject, $sAttCode, $bExtendedOutput = false)
{
if ($sAttCode == 'id')
{
$value = $oObject->GetKey();
}
else
{
$sClass = get_class($oObject);
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
if ($oAttDef instanceof AttributeLinkedSet)
{
// Iterate on the set and build an array of array of attcode=>value
$oSet = $oObject->Get($sAttCode);
$value = array();
while ($oLnk = $oSet->Fetch())
{
$sLnkRefClass = $bExtendedOutput ? get_class($oLnk) : $oAttDef->GetLinkedClass();
$aLnkValues = array();
foreach (MetaModel::ListAttributeDefs($sLnkRefClass) as $sLnkAttCode => $oLnkAttDef)
{
// Skip attributes pointing to the current object (redundant data)
if ($sLnkAttCode == $oAttDef->GetExtKeyToMe())
{
continue;
}
// Skip any attribute of the link that points to the current object
$oLnkAttDef = MetaModel::GetAttributeDef($sLnkRefClass, $sLnkAttCode);
if (method_exists($oLnkAttDef, 'GetKeyAttCode'))
{
if ($oLnkAttDef->GetKeyAttCode() == $oAttDef->GetExtKeyToMe())
{
continue;
}
}
$aLnkValues[$sLnkAttCode] = $this->MakeResultValue($oLnk, $sLnkAttCode, $bExtendedOutput);
}
$value[] = $aLnkValues;
}
}
else
{
$value = $oAttDef->GetForJSON($oObject->Get($sAttCode));
}
}
return $value;
}
/**
* Report the value for the given object attribute
*
* @param DBObject $oObject The object being reported
* @param string $sAttCode The attribute code (must be valid)
* @param boolean $bExtendedOutput Output all of the link set attributes ?
* @return void
*/
public function AddField(DBObject $oObject, $sAttCode, $bExtendedOutput = false)
{
$this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode, $bExtendedOutput);
}
}
/**
* REST response for services managing objects. Derive this structure to add information and/or constants
*
* @package Extensibility
* @package REST Services
* @api
*/
class RestResultWithObjects extends RestResult
{
public $objects;
/**
* Report the given object
*
* @param int An error code (RestResult::OK is no issue has been found)
* @param string $sMessage Description of the error if any, an empty string otherwise
* @param DBObject $oObject The object being reported
* @param array $aFieldSpec An array of class => attribute codes (Cf. RestUtils::GetFieldList). List of the attributes to be reported.
* @param boolean $bExtendedOutput Output all of the link set attributes ?
* @return void
*/
public function AddObject($iCode, $sMessage, $oObject, $aFieldSpec = null, $bExtendedOutput = false)
{
$sClass = get_class($oObject);
$oObjRes = new ObjectResult($sClass, $oObject->GetKey());
$oObjRes->code = $iCode;
$oObjRes->message = $sMessage;
$aFields = null;
if (!is_null($aFieldSpec))
{
// Enum all classes in the hierarchy, starting with the current one
foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, false) as $sRefClass)
{
if (array_key_exists($sRefClass, $aFieldSpec))
{
$aFields = $aFieldSpec[$sRefClass];
break;
}
}
}
if (is_null($aFields))
{
// No fieldspec given, or not found...
$aFields = array('id', 'friendlyname');
}
foreach ($aFields as $sAttCode)
{
$oObjRes->AddField($oObject, $sAttCode, $bExtendedOutput);
}
$sObjKey = get_class($oObject).'::'.$oObject->GetKey();
$this->objects[$sObjKey] = $oObjRes;
}
}
class RestResultWithRelations extends RestResultWithObjects
{
public $relations;
public function __construct()
{
parent::__construct();
$this->relations = array();
}
public function AddRelation($sSrcKey, $sDestKey)
{
if (!array_key_exists($sSrcKey, $this->relations))
{
$this->relations[$sSrcKey] = array();
}
$this->relations[$sSrcKey][] = array('key' => $sDestKey);
}
}
/**
* Deletion result codes for a target object (either deleted or updated)
*
* @package Extensibility
* @api
* @since 2.0.1
*/
class RestDelete
{
/**
* Result: Object deleted as per the initial request
*/
const OK = 0;
/**
* Result: general issue (user rights or ... ?)
*/
const ISSUE = 1;
/**
* Result: Must be deleted to preserve database integrity
*/
const AUTO_DELETE = 2;
/**
* Result: Must be deleted to preserve database integrity, but that is NOT possible
*/
const AUTO_DELETE_ISSUE = 3;
/**
* Result: Must be deleted to preserve database integrity, but this must be requested explicitely
*/
const REQUEST_EXPLICITELY = 4;
/**
* Result: Must be updated to preserve database integrity
*/
const AUTO_UPDATE = 5;
/**
* Result: Must be updated to preserve database integrity, but that is NOT possible
*/
const AUTO_UPDATE_ISSUE = 6;
}
/**
* Implementation of core REST services (create/get/update... objects)
*
* @package Core
*/
class CoreServices implements iRestServiceProvider
{
/**
* Enumerate services delivered by this class
*
* @param string $sVersion The version (e.g. 1.0) supported by the services
* @return array An array of hash 'verb' => verb, 'description' => description
*/
public function ListOperations($sVersion)
{
// 1.1 - In the reply, objects have a 'key' entry so that it is no more necessary to split class::key programmaticaly
//
$aOps = array();
if (in_array($sVersion, array('1.0', '1.1')))
{
$aOps[] = array(
'verb' => 'core/create',
'description' => 'Create an object'
);
$aOps[] = array(
'verb' => 'core/update',
'description' => 'Update an object'
);
$aOps[] = array(
'verb' => 'core/apply_stimulus',
'description' => 'Apply a stimulus to change the state of an object'
);
$aOps[] = array(
'verb' => 'core/get',
'description' => 'Search for objects'
);
$aOps[] = array(
'verb' => 'core/delete',
'description' => 'Delete objects'
);
$aOps[] = array(
'verb' => 'core/get_related',
'description' => 'Get related objects through the specified relation'
);
$aOps[] = array(
'verb' => 'core/check_credentials',
'description' => 'Check user credentials'
);
}
return $aOps;
}
/**
* Enumerate services delivered by this class
* @param string $sVersion The version (e.g. 1.0) supported by the services
* @return RestResult The standardized result structure (at least a message)
* @throws Exception in case of internal failure.
*/
public function ExecOperation($sVersion, $sVerb, $aParams)
{
$oResult = new RestResultWithObjects();
switch ($sVerb)
{
case 'core/create':
RestUtils::InitTrackingComment($aParams);
$sClass = RestUtils::GetClass($aParams, 'class');
$aFields = RestUtils::GetMandatoryParam($aParams, 'fields');
$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
$bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+');
$oObject = RestUtils::MakeObjectFromFields($sClass, $aFields);
$oObject->DBInsert();
$oResult->AddObject(0, 'created', $oObject, $aShowFields, $bExtendedOutput);
break;
case 'core/update':
RestUtils::InitTrackingComment($aParams);
$sClass = RestUtils::GetClass($aParams, 'class');
$key = RestUtils::GetMandatoryParam($aParams, 'key');
$aFields = RestUtils::GetMandatoryParam($aParams, 'fields');
$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
$bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+');
$oObject = RestUtils::FindObjectFromKey($sClass, $key);
RestUtils::UpdateObjectFromFields($oObject, $aFields);
$oObject->DBUpdate();
$oResult->AddObject(0, 'updated', $oObject, $aShowFields, $bExtendedOutput);
break;
case 'core/apply_stimulus':
RestUtils::InitTrackingComment($aParams);
$sClass = RestUtils::GetClass($aParams, 'class');
$key = RestUtils::GetMandatoryParam($aParams, 'key');
$aFields = RestUtils::GetMandatoryParam($aParams, 'fields');
$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
$bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+');
$sStimulus = RestUtils::GetMandatoryParam($aParams, 'stimulus');
$oObject = RestUtils::FindObjectFromKey($sClass, $key);
RestUtils::UpdateObjectFromFields($oObject, $aFields);
$aTransitions = $oObject->EnumTransitions();
$aStimuli = MetaModel::EnumStimuli(get_class($oObject));
if (!isset($aTransitions[$sStimulus]))
{
// Invalid stimulus
$oResult->code = RestResult::INTERNAL_ERROR;
$oResult->message = "Invalid stimulus: '$sStimulus' on the object ".$oObject->GetName()." in state '".$oObject->GetState()."'";
}
else
{
$aTransition = $aTransitions[$sStimulus];
$sTargetState = $aTransition['target_state'];
$aStates = MetaModel::EnumStates($sClass);
$aTargetStateDef = $aStates[$sTargetState];
$aExpectedAttributes = $aTargetStateDef['attribute_list'];
$aMissingMandatory = array();
foreach($aExpectedAttributes as $sAttCode => $iExpectCode)
{
if ( ($iExpectCode & OPT_ATT_MANDATORY) && ($oObject->Get($sAttCode) == ''))
{
$aMissingMandatory[] = $sAttCode;
}
}
if (count($aMissingMandatory) == 0)
{
// If all the mandatory fields are already present, just apply the transition silently...
if ($oObject->ApplyStimulus($sStimulus))
{
$oObject->DBUpdate();
$oResult->AddObject(0, 'updated', $oObject, $aShowFields, $bExtendedOutput);
}
}
else
{
// Missing mandatory attributes for the transition
$oResult->code = RestResult::INTERNAL_ERROR;
$oResult->message = 'Missing mandatory attribute(s) for applying the stimulus: '.implode(', ', $aMissingMandatory).'.';
}
}
break;
case 'core/get':
$sClass = RestUtils::GetClass($aParams, 'class');
$key = RestUtils::GetMandatoryParam($aParams, 'key');
$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
$bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+');
$oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key);
while ($oObject = $oObjectSet->Fetch())
{
$oResult->AddObject(0, '', $oObject, $aShowFields, $bExtendedOutput);
}
$oResult->message = "Found: ".$oObjectSet->Count();
break;
case 'core/delete':
$sClass = RestUtils::GetClass($aParams, 'class');
$key = RestUtils::GetMandatoryParam($aParams, 'key');
$bSimulate = RestUtils::GetOptionalParam($aParams, 'simulate', false);
$oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key);
$aObjects = $oObjectSet->ToArray();
$this->DeleteObjects($oResult, $aObjects, $bSimulate);
break;
case 'core/get_related':
$oResult = new RestResultWithRelations();
$sClass = RestUtils::GetClass($aParams, 'class');
$key = RestUtils::GetMandatoryParam($aParams, 'key');
$sRelation = RestUtils::GetMandatoryParam($aParams, 'relation');
$iMaxRecursionDepth = RestUtils::GetOptionalParam($aParams, 'depth', 20 /* = MAX_RECURSION_DEPTH */);
$oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key);
$aIndexByClass = array();
while ($oObject = $oObjectSet->Fetch())
{
$aRelated = array();
$aGraph = array();
$aIndexByClass[get_class($oObject)][$oObject->GetKey()] = null;
$oResult->AddObject(0, '', $oObject);
$this->GetRelatedObjects($oObject, $sRelation, $iMaxRecursionDepth, $aRelated, $aGraph);
foreach($aRelated as $sClass => $aObjects)
{
foreach($aObjects as $oRelatedObj)
{
$aIndexByClass[get_class($oRelatedObj)][$oRelatedObj->GetKey()] = null;
$oResult->AddObject(0, '', $oRelatedObj);
}
}
foreach($aGraph as $sSrcKey => $aDestinations)
{
foreach ($aDestinations as $sDestKey)
{
$oResult->AddRelation($sSrcKey, $sDestKey);
}
}
}
if (count($aIndexByClass) > 0)
{
$aStats = array();
foreach ($aIndexByClass as $sClass => $aIds)
{
$aStats[] = $sClass.'= '.count($aIds);
}
$oResult->message = "Scope: ".$oObjectSet->Count()."; Related objects: ".implode(', ', $aStats);
}
else
{
$oResult->message = "Nothing found";
}
break;
case 'core/check_credentials':
$oResult = new RestResult();
$sUser = RestUtils::GetMandatoryParam($aParams, 'user');
$sPassword = RestUtils::GetMandatoryParam($aParams, 'password');
if (UserRights::CheckCredentials($sUser, $sPassword) !== true)
{
$oResult->authorized = false;
}
else
{
$oResult->authorized = true;
}
break;
default:
// unknown operation: handled at a higher level
}
return $oResult;
}
/**
* Helper for object deletion
*/
public function DeleteObjects($oResult, $aObjects, $bSimulate)
{
$oDeletionPlan = new DeletionPlan();
foreach($aObjects as $oObj)
{
if ($bSimulate)
{
$oObj->CheckToDelete($oDeletionPlan);
}
else
{
$oObj->DBDelete($oDeletionPlan);
}
}
foreach ($oDeletionPlan->ListDeletes() as $sTargetClass => $aDeletes)
{
foreach ($aDeletes as $iId => $aData)
{
$oToDelete = $aData['to_delete'];
$bAutoDel = (($aData['mode'] == DEL_SILENT) || ($aData['mode'] == DEL_AUTO));
if (array_key_exists('issue', $aData))
{
if ($bAutoDel)
{
if (isset($aData['requested_explicitely'])) // i.e. in the initial list of objects to delete
{
$iCode = RestDelete::ISSUE;
$sPlanned = 'Cannot be deleted: '.$aData['issue'];
}
else
{
$iCode = RestDelete::AUTO_DELETE_ISSUE;
$sPlanned = 'Should be deleted automatically... but: '.$aData['issue'];
}
}
else
{
$iCode = RestDelete::REQUEST_EXPLICITELY;
$sPlanned = 'Must be deleted explicitely... but: '.$aData['issue'];
}
}
else
{
if ($bAutoDel)
{
if (isset($aData['requested_explicitely']))
{
$iCode = RestDelete::OK;
$sPlanned = '';
}
else
{
$iCode = RestDelete::AUTO_DELETE;
$sPlanned = 'Deleted automatically';
}
}
else
{
$iCode = RestDelete::REQUEST_EXPLICITELY;
$sPlanned = 'Must be deleted explicitely';
}
}
$oResult->AddObject($iCode, $sPlanned, $oToDelete);
}
}
foreach ($oDeletionPlan->ListUpdates() as $sRemoteClass => $aToUpdate)
{
foreach ($aToUpdate as $iId => $aData)
{
$oToUpdate = $aData['to_reset'];
if (array_key_exists('issue', $aData))
{
$iCode = RestDelete::AUTO_UPDATE_ISSUE;
$sPlanned = 'Should be updated automatically... but: '.$aData['issue'];
}
else
{
$iCode = RestDelete::AUTO_UPDATE;
$sPlanned = 'Reset external keys: '.$aData['attributes_list'];
}
$oResult->AddObject($iCode, $sPlanned, $oToUpdate);
}
}
if ($oDeletionPlan->FoundStopper())
{
if ($oDeletionPlan->FoundSecurityIssue())
{
$iRes = RestResult::UNAUTHORIZED;
$sRes = 'Deletion not allowed on some objects';
}
elseif ($oDeletionPlan->FoundManualOperation())
{
$iRes = RestResult::UNSAFE;
$sRes = 'The deletion requires that other objects be deleted/updated, and those operations must be requested explicitely';
}
else
{
$iRes = RestResult::INTERNAL_ERROR;
$sRes = 'Some issues have been encountered. See the list of planned changes for more information about the issue(s).';
}
}
else
{
$iRes = RestResult::OK;
$sRes = 'Deleted: '.count($aObjects);
$iIndirect = $oDeletionPlan->GetTargetCount() - count($aObjects);
if ($iIndirect > 0)
{
$sRes .= ' plus (for DB integrity) '.$iIndirect;
}
}
$oResult->code = $iRes;
if ($bSimulate)
{
$oResult->message = 'SIMULATING: '.$sRes;
}
else
{
$oResult->message = $sRes;
}
}
/**
* Helper function to get the related objects up to the given depth along with the "graph" of the relation
* @param DBObject $oObject Starting point of the computation
* @param string $sRelation Code of the relation (i.e; 'impact', 'depends on'...)
* @param integer $iMaxRecursionDepth Maximum level of recursion
* @param Hash $aRelated Two dimensions hash of the already related objects: array( 'class' => array(key => ))
* @param Hash $aGraph Hash array for the topology of the relation: source => related: array('class:key' => array( DBObjects ))
* @param integer $iRecursionDepth Current level of recursion
*/
protected function GetRelatedObjects(DBObject $oObject, $sRelation, $iMaxRecursionDepth, &$aRelated, &$aGraph, $iRecursionDepth = 1)
{
// Avoid loops
if ((array_key_exists(get_class($oObject), $aRelated)) && (array_key_exists($oObject->GetKey(), $aRelated[get_class($oObject)]))) return;
// Stop at maximum recursion level
if ($iRecursionDepth > $iMaxRecursionDepth) return;
$sSrcKey = get_class($oObject).'::'.$oObject->GetKey();
$aNewRelated = array();
$oObject->GetRelatedObjects($sRelation, 1, $aNewRelated);
foreach($aNewRelated as $sClass => $aObjects)
{
if (!array_key_exists($sSrcKey, $aGraph))
{
$aGraph[$sSrcKey] = array();
}
foreach($aObjects as $oRelatedObject)
{
$aRelated[$sClass][$oRelatedObject->GetKey()] = $oRelatedObject;
$aGraph[$sSrcKey][] = get_class($oRelatedObject).'::'.$oRelatedObject->GetKey();
$this->GetRelatedObjects($oRelatedObject, $sRelation, $iMaxRecursionDepth, $aRelated, $aGraph, $iRecursionDepth+1);
}
}
}
}

View File

@@ -488,8 +488,6 @@ class SQLQuery
private function privRenderSingleTable(&$aFrom, &$aFields, &$aGroupBy, &$aDelTables, &$aSetValues, &$aSelectedIdFields, $sCallerAlias = '', $aJoinData)
{
$aActualTableFields = CMDBSource::GetTableFieldsList($this->m_sTable);
$aTranslationTable[$this->m_sTable]['*'] = $this->m_sTableAlias;
// Handle the various kinds of join (or first table in the list)

View File

@@ -64,7 +64,7 @@ abstract class Trigger extends cmdbAbstractObject
public function DoActivate($aContextArgs)
{
// Find the related
// Find the related actions
$oLinkedActions = $this->Get('action_list');
while ($oLink = $oLinkedActions->Fetch())
{
@@ -76,6 +76,19 @@ abstract class Trigger extends cmdbAbstractObject
}
}
}
/**
* Check whether the given object is in the scope of this trigger
* and can potentially be the subject of notifications
* @param DBObject $oObject The object to check
* @return bool
*/
public function IsInScope(DBObject $oObject)
{
// By default the answer is no
// Overload this function in your own derived class for a different behavior
return false;
}
}
abstract class TriggerOnObject extends Trigger
@@ -96,15 +109,81 @@ abstract class TriggerOnObject extends Trigger
);
MetaModel::Init_Params($aParams);
MetaModel::Init_InheritAttributes();
MetaModel::Init_AddAttribute(new AttributeClass("target_class", array("class_category"=>"bizmodel", "more_values"=>null, "sql"=>"target_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeClass("target_class", array("class_category"=>"bizmodel", "more_values"=>"User,UserExternal,UserInternal,UserLDAP,UserLocal", "sql"=>"target_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeOQL("filter", array("allowed_values"=>null, "sql"=>"filter", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
// Display lists
MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'action_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'action_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class', 'description')); // Attributes to be displayed for a list
// Search criteria
// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
}
public function DoCheckToWrite()
{
parent::DoCheckToWrite();
$sFilter = trim($this->Get('filter'));
if (strlen($sFilter) > 0)
{
try
{
$oSearch = DBObjectSearch::FromOQL($sFilter);
if (!MetaModel::IsParentClass($this->Get('target_class'), $oSearch->GetClass()))
{
$this->m_aCheckIssues[] = Dict::Format('TriggerOnObject:WrongFilterClass', $this->Get('target_class'));
}
}
catch(OqlException $e)
{
$this->m_aCheckIssues[] = Dict::Format('TriggerOnObject:WrongFilterQuery', $e->getMessage());
}
}
}
/**
* Check whether the given object is in the scope of this trigger
* and can potentially be the subject of notifications
* @param DBObject $oObject The object to check
* @return bool
*/
public function IsInScope(DBObject $oObject)
{
$sRootClass = $this->Get('target_class');
return ($oObject instanceof $sRootClass);
}
public function DoActivate($aContextArgs)
{
$bGo = true;
if (isset($aContextArgs['this->id']))
{
$bGo = $this->IsTargetObject($aContextArgs['this->id']);
}
if ($bGo)
{
parent::DoActivate($aContextArgs);
}
}
public function IsTargetObject($iObjectId)
{
$sFilter = trim($this->Get('filter'));
if (strlen($sFilter) > 0)
{
$oSearch = DBObjectSearch::FromOQL($sFilter);
$oSearch->AddCondition('id', $iObjectId, '=');
$oSet = new DBObjectSet($oSearch);
$bRet = ($oSet->Count() > 0);
}
else
{
$bRet = true;
}
return $bRet;
}
}
/**
* To trigger notifications when a ticket is updated from the portal
@@ -129,7 +208,7 @@ class TriggerOnPortalUpdate extends TriggerOnObject
MetaModel::Init_InheritAttributes();
// Display lists
MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'action_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'action_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class', 'description')); // Attributes to be displayed for a list
// Search criteria
}
@@ -156,7 +235,7 @@ abstract class TriggerOnStateChange extends TriggerOnObject
MetaModel::Init_AddAttribute(new AttributeString("state", array("allowed_values"=>null, "sql"=>"state", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
// Display lists
MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'state', 'action_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'state', 'action_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class', 'state')); // Attributes to be displayed for a list
// Search criteria
MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class', 'state')); // Criteria of the std search form
@@ -184,7 +263,7 @@ class TriggerOnStateEnter extends TriggerOnStateChange
MetaModel::Init_InheritAttributes();
// Display lists
MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'state', 'action_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'state', 'action_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', array('target_class', 'state')); // Attributes to be displayed for a list
// Search criteria
MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class', 'state')); // Criteria of the std search form
@@ -212,7 +291,7 @@ class TriggerOnStateLeave extends TriggerOnStateChange
MetaModel::Init_InheritAttributes();
// Display lists
MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'state', 'action_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'state', 'action_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', array('target_class', 'state')); // Attributes to be displayed for a list
// Search criteria
MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class', 'state')); // Criteria of the std search form
@@ -240,7 +319,7 @@ class TriggerOnObjectCreate extends TriggerOnObject
MetaModel::Init_InheritAttributes();
// Display lists
MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'action_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'action_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class')); // Attributes to be displayed for a list
// Search criteria
MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class')); // Criteria of the std search form

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -66,6 +66,86 @@ abstract class UserRightsAddOnAPI
abstract public function IsAdministrator($oUser);
abstract public function IsPortalUser($oUser);
abstract public function FlushPrivileges();
/**
* ...
*/
public function MakeSelectFilter($sClass, $aAllowedOrgs, $aSettings = array(), $sAttCode = null)
{
if ($sAttCode == null)
{
$sAttCode = $this->GetOwnerOrganizationAttCode($sClass);
}
if (empty($sAttCode))
{
return $oFilter = new DBObjectSearch($sClass);
}
$oExpression = new FieldExpression($sAttCode, $sClass);
$oFilter = new DBObjectSearch($sClass);
$oListExpr = ListExpression::FromScalars($aAllowedOrgs);
$oCondition = new BinaryExpression($oExpression, 'IN', $oListExpr);
$oFilter->AddConditionExpression($oCondition);
if ($this->HasSharing())
{
if (($sAttCode == 'id') && isset($aSettings['bSearchMode']) && $aSettings['bSearchMode'])
{
// Querying organizations (or derived)
// and the expected list of organizations will be used as a search criteria
// Therefore the query can also return organization having objects shared with the allowed organizations
//
// 1) build the list of organizations sharing something with the allowed organizations
// Organization <== sharing_org_id == SharedObject having org_id IN {user orgs}
$oShareSearch = new DBObjectSearch('SharedObject');
$oOrgField = new FieldExpression('org_id', 'SharedObject');
$oShareSearch->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr));
$oSearchSharers = new DBObjectSearch('Organization');
$oSearchSharers->AllowAllData();
$oSearchSharers->AddCondition_ReferencedBy($oShareSearch, 'sharing_org_id');
$aSharers = array();
foreach($oSearchSharers->ToDataArray(array('id')) as $aRow)
{
$aSharers[] = $aRow['id'];
}
// 2) Enlarge the overall results: ... OR id IN(id1, id2, id3)
if (count($aSharers) > 0)
{
$oSharersList = ListExpression::FromScalars($aSharers);
$oFilter->MergeConditionExpression(new BinaryExpression($oExpression, 'IN', $oSharersList));
}
}
$aShareProperties = SharedObject::GetSharedClassProperties($sClass);
if ($aShareProperties)
{
$sShareClass = $aShareProperties['share_class'];
$sShareAttCode = $aShareProperties['attcode'];
$oSearchShares = new DBObjectSearch($sShareClass);
$oSearchShares->AllowAllData();
$sHierarchicalKeyCode = MetaModel::IsHierarchicalClass('Organization');
$oOrgField = new FieldExpression('org_id', $sShareClass);
$oSearchShares->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr));
$aShared = array();
foreach($oSearchShares->ToDataArray(array($sShareAttCode)) as $aRow)
{
$aShared[] = $aRow[$sShareAttCode];
}
if (count($aShared) > 0)
{
$oObjId = new FieldExpression('id', $sClass);
$oSharedIdList = ListExpression::FromScalars($aShared);
$oFilter->MergeConditionExpression(new BinaryExpression($oObjId, 'IN', $oSharedIdList));
}
}
} // if HasSharing
return $oFilter;
}
}
@@ -140,6 +220,7 @@ abstract class User extends cmdbAbstractObject
return $sLastName;
}
}
return $this->Get('login');
}
/*
@@ -289,6 +370,9 @@ abstract class UserInternal extends User
MetaModel::Init_Params($aParams);
MetaModel::Init_InheritAttributes();
// When set, this token allows for password reset
MetaModel::Init_AddAttribute(new AttributeString("reset_pwd_token", array("allowed_values"=>null, "sql"=>"reset_pwd_token", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
// Display lists
MetaModel::Init_SetZListItems('details', array('contactid', 'first_name', 'email', 'login', 'language', 'profile_list', 'allowed_org_list')); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', array('finalclass', 'first_name', 'last_name', 'login')); // Attributes to be displayed for a list
@@ -296,6 +380,47 @@ abstract class UserInternal extends User
MetaModel::Init_SetZListItems('standard_search', array('login', 'contactid')); // Criteria of the std search form
MetaModel::Init_SetZListItems('advanced_search', array('login', 'contactid')); // Criteria of the advanced search form
}
/**
* Use with care!
*/
public function SetPassword($sNewPassword)
{
}
/**
* The email recipient is the person who is allowed to regain control when the password gets lost
* Throws an exception if the feature cannot be available
*/
public function GetResetPasswordEmail()
{
if (!MetaModel::IsValidAttCode(get_class($this), 'contactid'))
{
throw new Exception(Dict::S('UI:ResetPwd-Error-NoContact'));
}
$iContactId = $this->Get('contactid');
if ($iContactId == 0)
{
throw new Exception(Dict::S('UI:ResetPwd-Error-NoContact'));
}
$oContact = MetaModel::GetObject('Contact', $iContactId);
// Determine the email attribute (the first one will be our choice)
foreach (MetaModel::ListAttributeDefs(get_class($oContact)) as $sAttCode => $oAttDef)
{
if ($oAttDef instanceof AttributeEmailAddress)
{
$sEmailAttCode = $sAttCode;
// we've got one, exit the loop
break;
}
}
if (!isset($sEmailAttCode))
{
throw new Exception(Dict::S('UI:ResetPwd-Error-NoEmailAtt'));
}
$sRes = trim($oContact->Get($sEmailAttCode));
return $sRes;
}
}
/**
@@ -667,6 +792,7 @@ class UserRights
}
}
public static function IsActionAllowed($sClass, $iActionCode, /*dbObjectSet*/ $oInstanceSet = null, $oUser = null)
{
// When initializing, we need to let everything pass trough
@@ -873,6 +999,11 @@ class UserRights
}
return $oUser;
}
public static function MakeSelectFilter($sClass, $aAllowedOrgs, $aSettings = array(), $sAttCode = null)
{
return self::$m_oAddOn->MakeSelectFilter($sClass, $aAllowedOrgs, $aSettings, $sAttCode);
}
}
/**

View File

@@ -382,14 +382,34 @@ class ValueSetEnumClasses extends ValueSetEnum
}
protected function LoadValues($aArgs)
{
// First, get the additional values
{
// Call the parent to parse the additional values...
parent::LoadValues($aArgs);
// Translate the labels of the additional values
foreach($this->m_aValues as $sClass => $void)
{
if (MetaModel::IsValidClass($sClass))
{
$this->m_aValues[$sClass] = MetaModel::GetName($sClass);
}
else
{
unset($this->m_aValues[$sClass]);
}
}
// Then, add the classes from the category definition
foreach (MetaModel::GetClasses($this->m_sCategories) as $sClass)
{
$this->m_aValues[$sClass] = MetaModel::GetName($sClass);
if (MetaModel::IsValidClass($sClass))
{
$this->m_aValues[$sClass] = MetaModel::GetName($sClass);
}
else
{
unset($this->m_aValues[$sClass]);
}
}
return true;

View File

@@ -8,6 +8,11 @@ body {
overflow: hidden; /* Remove scroll bars on browser window */
}
/* to prevent flicker, hide the pane's content until it's ready */
.ui-layout-center, .ui-layout-north, .ui-layout-south {
display: none;
}
.raw_output {
font-family: Courier-New, Courier, Arial, Helvetica;
font-size: 8pt;
@@ -1102,9 +1107,6 @@ table.pagination tr td {
.pagination_container {
padding-left: 3px;
}
.pager {
display:inline-block;
}
.pager p {
margin-top: 0;
margin-bottom: 0;
@@ -1409,4 +1411,8 @@ a.summary, a.summary:hover {
}
.fg-menu a img {
border: 0;
}
}
div.ui-dialog-header {
padding-bottom: 10px;
padding-top: 7px;
}

56
css/login.css Normal file
View File

@@ -0,0 +1,56 @@
@CHARSET "UTF-8";
body {
background: #eee;
margin: 0;
padding: 0;
}
#login-logo {
margin-top: 150px;
width: 300px;
padding-left: 20px;
padding-right: 20px;
padding-top: 10px;
padding-bottom: 10px;
margin-left: auto;
margin-right: auto;
background: #f6f6f1;
height: 54px;
border-top: 1px solid #000;
border-left: 1px solid #000;
border-right: 1px solid #000;
border-bottom: 0;
text-align: center;
}
#login-logo img {
border: 0;
}
#login {
width: 300px;
margin-left: auto;
margin-right: auto;
padding: 20px;
background-color: #fff;
border-bottom: 1px solid #000;
border-left: 1px solid #000;
border-right: 1px solid #000;
border-top: 0;
text-align: center;
}
#login table {
width: 100%;
}
#pwd, #user,#old_pwd, #new_pwd, #retype_new_pwd {
width: 10em;
}
.center {
text-align: center;
}
h1 {
color: #1C94C4;
font-size: 16pt;
}
.v-spacer {
padding-top: 1em;
}

View File

@@ -1,15 +1,15 @@
@CHARSET "UTF-8";
#left-pane { display: none; }
span.ui-layout-resizer { display: none; }
#header-logo { display: none; }
#logo { display: none; }
div.header-menu { display:none; }
div.footer { display:none; }
#top-bar { display: none; }
#menu { display: none; }
#inner_menu { display: none; }
div.actions_button { display:none; }
div.itop_popup { display:none; }
div.HRDrawer { display:none; }
div.DrawerHandle { display:none; }
a.tab { display:none; }
div.itop-tab { border: #ccc 1px solid; margin-top: 1em; padding-bottom:1em; }
div.itop-tab { border: #ccc 1px solid; margin-top: 1em; padding-bottom:1em; }
#combodo_logo { display:none; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 B

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 B

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 B

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 B

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 B

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 B

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

File diff suppressed because one or more lines are too long

View File

@@ -16,36 +16,17 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* Localized data
*
* @author Stephan Rosenke <stephan.rosenke@itomig.de>
* @author Stephan Rosenke <stephan.rosenke@itomig.de>
* @author David M. Gümbel <david.guembel@itomig.de>
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
* @licence http://opensource.org/licenses/AGPL-3.0
*/
// Dictionnay conventions
// Class:<class_name>
// Class:<class_name>+
// Class:<class_name>/Attribute:<attribute_code>
// Class:<class_name>/Attribute:<attribute_code>+
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
// Class:<class_name>/Stimulus:<stimulus_code>
// Class:<class_name>/Stimulus:<stimulus_code>+
//
// Class: UserLocal
//
Dict::Add('DE DE', 'German', 'Deutsch', array(
'Class:UserLocal' => 'iTop-Benutzer',
'Class:UserLocal+' => 'Benutzer von iTop authentifiziert',
'Class:UserLocal/Attribute:password' => 'Passwort',
'Class:UserLocal/Attribute:password+' => 'Benutzerpasswort',
));
?>
?>

View File

@@ -0,0 +1,51 @@
<?php
// Copyright (C) 2010-2013 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/>
/**
* Localized data
*
* @copyright Copyright (C) 2010-2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
* @traductor Miguel Turrubiates <miguel_tf@yahoo.com>
*/
// Dictionnay conventions
// Class:<class_name>
// Class:<class_name>+
// Class:<class_name>/Attribute:<attribute_code>
// Class:<class_name>/Attribute:<attribute_code>+
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
// Class:<class_name>/Stimulus:<stimulus_code>
// Class:<class_name>/Stimulus:<stimulus_code>+
//
// Class: UserLocal
//
Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
'Class:UserLocal' => 'Usuario de iTop',
'Class:UserLocal+' => 'Usuario Autenticado vía iTop',
'Class:UserLocal/Attribute:password' => 'Contrase&ntilde;a',
'Class:UserLocal/Attribute:password+' => 'Contrase&ntilde;a',
));
?>

View File

@@ -1,51 +1,32 @@
<?php
// Copyright (C) 2010-2012 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/>
/**
* Localized data
*
* @author Hirofumi Kosaka <kosaka@rworks.jp>
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
// Dictionnay conventions
// Class:<class_name>
// Class:<class_name>+
// Class:<class_name>/Attribute:<attribute_code>
// Class:<class_name>/Attribute:<attribute_code>+
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
// Class:<class_name>/Stimulus:<stimulus_code>
// Class:<class_name>/Stimulus:<stimulus_code>+
//
// Class: UserLocal
//
Dict::Add('JA JP', 'Japanese', '日本語', array(
'Class:UserLocal' => 'iTopユーザー', // 'iTop user',
'Class:UserLocal+' => 'iTopローカル認証ユーザー', // 'User authentified by iTop',
'Class:UserLocal/Attribute:password' => 'パスワード', // 'Password',
'Class:UserLocal/Attribute:password+' => '認証文字列', // 'user authentication string',
));
?>
<?php
// Copyright (C) 2010-2012 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/>
/**
* @author Hirofumi Kosaka <kosaka@rworks.jp>
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @licence http://opensource.org/licenses/AGPL-3.0
*/
Dict::Add('JA JP', 'Japanese', '日本語', array(
'Class:UserLocal' => 'iTopユーザー',
'Class:UserLocal+' => 'iTopローカル認証ユーザー',
'Class:UserLocal/Attribute:password' => 'パスワード',
'Class:UserLocal/Attribute:password+' => '認証文字列',
));
?>

View File

@@ -74,7 +74,10 @@ class UserLocal extends UserInternal
public function CanChangePassword()
{
// For now everyone can change their password..
if (MetaModel::GetConfig()->Get('demo_mode'))
{
return false;
}
return true;
}
@@ -85,18 +88,47 @@ class UserLocal extends UserInternal
// Let's ask the password to compare the hashed values
if ($oPassword->CheckPassword($sOldPassword))
{
$this->Set('password', $sNewPassword);
$oChange = MetaModel::NewObject("CMDBChange");
$oChange->Set("date", time());
$sUserString = CMDBChange::GetCurrentUserName();
$oChange->Set("userinfo", $sUserString);
$oChange->DBInsert();
$this->DBUpdateTracked($oChange, true);
$this->SetPassword($sNewPassword);
return true;
}
return false;
}
/**
* Use with care!
*/
public function SetPassword($sNewPassword)
{
$this->Set('password', $sNewPassword);
$oChange = MetaModel::NewObject("CMDBChange");
$oChange->Set("date", time());
$sUserString = CMDBChange::GetCurrentUserName();
$oChange->Set("userinfo", $sUserString);
$oChange->DBInsert();
$this->DBUpdateTracked($oChange, true);
}
/**
* Returns the set of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...)
* for the given attribute in the current state of the object
* @param $sAttCode string $sAttCode The code of the attribute
* @param $aReasons array To store the reasons why the attribute is read-only (info about the synchro replicas)
* @param $sTargetState string The target state in which to evalutate the flags, if empty the current state will be used
* @return integer Flags: the binary combination of the flags applicable to this attribute
*/
public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '')
{
$iFlags = parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState);
if (MetaModel::GetConfig()->Get('demo_mode'))
{
if (strpos('contactid,login,language,password,profile_list,allowed_org_list', $sAttCode) !== false)
{
// contactid and allowed_org_list are disabled to make sure the portal remains accessible
$aReasons[] = 'Sorry, this attribute is read-only in the demonstration mode!';
$iFlags |= OPT_ATT_READONLY;
}
}
return $iFlags;
}
}
?>

View File

@@ -17,32 +17,16 @@
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* Localized data
*
* @author Vladimir Shilov <shilow@ukr.net>
* @author Vladimir Shilov <shilow@ukr.net>
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
* @licence http://opensource.org/licenses/AGPL-3.0
*/
// Dictionnay conventions
// Class:<class_name>
// Class:<class_name>+
// Class:<class_name>/Attribute:<attribute_code>
// Class:<class_name>/Attribute:<attribute_code>+
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
// Class:<class_name>/Stimulus:<stimulus_code>
// Class:<class_name>/Stimulus:<stimulus_code>+
//
// Class: UserLocal
//
Dict::Add('RU RU', 'Russian', 'Русский', array(
'Class:UserLocal' => 'Пользователь iTop',
'Class:UserLocal+' => 'Пользователь аутентифицированный iTop',
'Class:UserLocal/Attribute:password' => 'Пароль',
'Class:UserLocal/Attribute:password+' => 'строка аутентификации пользователя',
));
?>
?>

View File

@@ -35,6 +35,19 @@
<attribute id=""/>
</attributes>
</reconciliation>
<indexes>
<index id="1">
<attributes>
<attribute id="temp_id"/>
</attributes>
</index>
<index id="2">
<attributes>
<attribute id="item_class"/>
<attribute id="item_id"/>
</attributes>
</index>
</indexes>
</properties>
<fields>
<field id="expire" xsi:type="AttributeDateTime">

View File

@@ -45,8 +45,7 @@ SetupWebPage::AddModule(
),
'dictionary' => array(
'en.dict.attachments.php',
'fr.dict.attachments.php',
),
'data.struct' => array(
// add your 'structure' definition XML files here,

View File

@@ -5292,7 +5292,46 @@
<menu id="Change:Overview" xsi:type="DashboardMenuNode" _delta="define">
<rank>0</rank>
<parent>ChangeManagement</parent>
<definition_file>overview.xml</definition_file>
<definition>
<layout>DashboardLayoutTwoCols</layout>
<title></title>
<cells>
<cell id="0">
<rank>0</rank>
<dashlets>
<dashlet id="1" xsi:type="DashletGroupByBars">
<rank>0</rank>
<title>UI-ChangeManagementOverview-ChangeByType</title>
<query>SELECT Change</query>
<group_by>finalclass</group_by>
<style>bars</style>
</dashlet>
</dashlets>
</cell>
<cell id="1">
<rank>1</rank>
<dashlets>
<dashlet id="2" xsi:type="DashletObjectList">
<rank>0</rank>
<title>UI-ChangeManagementOverview-ChangeUnassigned</title>
<query>SELECT Change WHERE status = 'new'</query>
<menu>false</menu>
</dashlet>
</dashlets>
</cell>
<cell id="2">
<rank>2</rank>
<dashlets>
<dashlet id="3" xsi:type="DashletObjectList">
<rank>0</rank>
<title>UI-ChangeManagementOverview-ChangeWithOutage</title>
<query>SELECT Change WHERE outage = 'yes'</query>
<menu>false</menu>
</dashlet>
</dashlets>
</cell>
</cells>
</definition>
</menu>
<menu id="NewChange" xsi:type="NewObjectMenuNode" _delta="define">
<rank>1</rank>

View File

@@ -1,41 +0,0 @@
<?xml version="1.0"?>
<dashboard xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<layout>DashboardLayoutTwoCols</layout>
<title></title>
<cells>
<cell id="0">
<rank>0</rank>
<dashlets>
<dashlet id="1" xsi:type="DashletGroupByBars">
<rank>0</rank>
<title>UI-ChangeManagementOverview-ChangeByType</title>
<query>SELECT Change</query>
<group_by>finalclass</group_by>
<style>bars</style>
</dashlet>
</dashlets>
</cell>
<cell id="1">
<rank>1</rank>
<dashlets>
<dashlet id="2" xsi:type="DashletObjectList">
<rank>0</rank>
<title>UI-ChangeManagementOverview-ChangeUnassigned</title>
<query>SELECT Change WHERE status = 'new'</query>
<menu>false</menu>
</dashlet>
</dashlets>
</cell>
<cell id="2">
<rank>2</rank>
<dashlets>
<dashlet id="3" xsi:type="DashletObjectList">
<rank>0</rank>
<title>UI-ChangeManagementOverview-ChangeWithOutage</title>
<query>SELECT Change WHERE outage = 'yes'</query>
<menu>false</menu>
</dashlet>
</dashlets>
</cell>
</cells>
</dashboard>

View File

@@ -1277,14 +1277,15 @@
<type>Overload-cmdbAbstractObject</type>
<code><![CDATA[ public function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array())
{
$aFieldsMap = parent::DisplayBareProperties($oPage, $bEditMode, $sPrefix, $aExtraParams);
if (!$bEditMode)
{
$oPage->SetCurrentTab(Dict::S('Class:Document:PreviewTab'));
$oPage->add('<fieldset>');
$oPage->add('<legend>'.Dict::S('Class:Document:PreviewTab').'</legend>');
$oPage->add($this->DisplayDocumentInline($oPage, 'contents'));
$oPage->SetCurrentTab(Dict::S('UI:PropertiesTab'));
$oPage->add('</fieldset>');
}
parent::DisplayBareProperties($oPage, $bEditMode, $sPrefix, $aExtraParams);
return $aFieldsMap;
}]]></code>
</method>
</methods>
@@ -2121,6 +2122,7 @@
<attribute id="name"/>
<attribute id="org_id"/>
<attribute id="owner_name"/>
<attribute id="finalclass"/>
</attributes>
</reconciliation>
</properties>
@@ -2310,6 +2312,7 @@
<attribute id="device_name"/>
<attribute id="org_id"/>
<attribute id="owner_name"/>
<attribute id="finalclass"/>
</attributes>
</reconciliation>
</properties>
@@ -3231,6 +3234,7 @@
<attribute id="name"/>
<attribute id="org_id"/>
<attribute id="owner_name"/>
<attribute id="finalclass"/>
</attributes>
</reconciliation>
</properties>
@@ -5930,7 +5934,7 @@
</dashlet>
</dashlets>
</cell>
<cell id="0">
<cell id="1">
<rank>0</rank>
<dashlets>
<dashlet id="2" xsi:type="DashletGroupByBars">
@@ -5942,7 +5946,7 @@
</dashlet>
</dashlets>
</cell>
<cell id="0">
<cell id="2">
<rank>0</rank>
<dashlets>
<dashlet id="3" xsi:type="DashletGroupByTable">
@@ -5964,7 +5968,7 @@
<layout>DashboardLayoutOneCol</layout>
<title></title>
<cells>
<cell>
<cell id="1">
<rank>0</rank>
<dashlets>
<dashlet id="1" xsi:type="DashletHeaderDynamic">
@@ -5986,7 +5990,7 @@
</dashlet>
</dashlets>
</cell>
<cell>
<cell id="2">
<rank>1</rank>
<dashlets>
<dashlet id="4" xsi:type="DashletGroupByPie">

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