/*
* Copyright 2016 Anyware Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Root for all Ametys objects and methods. Also contains application parameters.<br/>
* <br/>
* Before loading this class you have to initialize the window.ametys_opt variable:
*
* <pre><code>
* // Theses options are here to initialize the Ametys object.
* // Do not use these since they are removed during Ametys initialization process
* window.ametys_opts = {
* "plugins-direct-prefix": '/_plugins', // See PLUGINS_DIRECT_PREFIX for details (private)
* "plugins-wrapped-prefix": '/plugins', // See PLUGINS_WRAPPED_PREFIX for details (private)
* "debug-mode": true, // See DEBUG_MODE for details
* "context-path": '', // See CONTEXT_PATH for details
* "workspace-name": 'admin', // See WORKSPACE_NAME for details
* "workspace-prefix": '/_admin', // See WORKSPACE_PREFIX for details
* "max-upload-size": '10670080', // See MAX_UPLOAD_SIZE for details
* "language-code": 'fr', // See LANGUAGE_CODE for details
* "rtl-mode": false, // See RTL_MODE for details
* "app-parameters: {} // See setAppParameter for details
* }
* </code></pre>
*
* <br/>
* Loading this class also initialize extjs with the following elements :
* <ul>
* <li>Test for authorized browser (redirecting if the browser is not supported),</li>
* <li>Removing window.ametys_authorized_browsers,</li>
* <li>Ext.Date.patterns are initialize to suitable values,</li>
* <li>Ext.BLANK_IMAGE_URL is associated to the correct url,</li>
* <li>Ext.SSL_SECURE_URL is associated to the correct url,</li>
* <li>Removing window.ametys_opts from global scope,</li>
* <li>Adding support for 'ametysDescription' config on fields and field container (that draws a ? image on the right of the fields),</li>
* <li>Doing some localization work,</li>
* <li>Display an hint under File field to display max upload size,</li>
* <li>Changing Ext.Ajax default request method from GET to POST, and setting timeout to 0.</li>
* </ul>
*/
Ext.define(
"Ametys",
{
singleton: true,
/**
* @property {String} CONTEXT_PATH The application context path. Can be empty for the root context path. E.g. '/MyContext'.
* @readonly
*/
CONTEXT_PATH: ametys_opts["context-path"] || "",
/**
* @property {String} WORKSPACE_NAME The name of the current ametys workspace. Cannot be empty. E.g. 'admin'
* @readonly
*/
WORKSPACE_NAME: ametys_opts["workspace-name"] || "",
/**
* @property {String} WORKSPACE_PREFIX The prefix of the current workspace (so not starting with the context path). If the workspace is the default one, this can be empty. E.g. '', '/_MyWorkspace'
* @readonly
*/
WORKSPACE_PREFIX: ametys_opts["workspace-prefix"] || "",
/**
* @property {String} WORKSPACE_URI The URI to go the current workspace (starting with the context path). If the context path is ROOT and the workspace is the default one, this can be empty. E.g. '', '/MyContext', '/MyContext/_MyWorkspace', '/_MyWorkspace'
* @readonly
*/
WORKSPACE_URI: (ametys_opts["context-path"] || "") + (ametys_opts["workspace-prefix"] || ""),
/**
* @property {String} MAX_UPLOAD_SIZE The parametrized max size for upadloding file to the server. In Bytes. E.g. 10670080 for 10 MB. Can be empty if unknown.
* @readonly
*/
MAX_UPLOAD_SIZE: ametys_opts["max-upload-size"] || 1000000,
/**
* @readonly
* @property {Boolean} DEBUG_MODE Load JS files in debug mode when available.
*/
DEBUG_MODE: ametys_opts["debug-mode"] || true,
/**
* @readonly
* @property {Boolean} RTL_MODE Load CSS files for right-to-left languages when available
*/
RTL_MODE: ametys_opts["debug-mode"] || false,
/**
* @property {String} LANGUAGE_CODE The language code supported when loading the application. E.g. 'en' or 'fr'
* @readonly
*/
LANGUAGE_CODE: ametys_opts["language-code"] || "",
/**
* @property {String} PLUGINS_DIRECT_PREFIX Prefix for direct url to plugins (used for AJAX connections) with leading '/' nor context path. e.g. '/_plugins'
* @readonly
* @private
*/
PLUGINS_DIRECT_PREFIX: ametys_opts["plugins-direct-prefix"] || "",
/**
* @property {String} PLUGINS_WRAPPED_PREFIX Prefix for wrapped url to plugins (used for redirections) with leading '/' nor context path. e.g. '/plugins'
* @readonly
* @private
*/
PLUGINS_WRAPPED_PREFIX: ametys_opts["plugins-wrapped-prefix"] || "",
/**
* @property {Object} appParameters Application parameters. Theses parameters are set by the application, and are added to all requests.
* They can also be used for local purposes.
* @private
*/
appParameters: ametys_opts["app-parameters"] || {},
/**
* @private
* @property {boolean} _off True if Ametys is shutdown
*/
/**
* @private
* @property {boolean} _unloading True if the browser is unloading the page
*/
/**
* Add an application parameter. Theses parameters are automatically added to all request, but are of course also added for local purposes.
* @param {String} name The key of the parameter
* @param {Object} value The value of the parameter. The object have to be encodable using Ext.JSON.encode
*/
setAppParameter: function(name, value) {
this.appParameters[name] = value;
Ext.Ajax.extraParams = "context.parameters=" + encodeURIComponent(Ext.JSON.encode(this.appParameters));
},
/**
* Get an application parameter.
* @param {String} name The key of the parameter
* @return {Object} The value associated to the parameter name, or undefined if it does not exist.
*/
getAppParameter: function(name) {
return this.appParameters[name];
},
/**
* Get every application parameters.
* @return {Object} The application parameters.
*/
getAppParameters: function() {
return Ext.clone(this.appParameters);
},
/**
* Get the url prefix for direct connection to a plugin (e.g. for ajax connections)
* @param {String} plugin The plugin name. Cannot be null or empty.
* @return {String} The url prefix for accessing directly plugins (e.g. for ajax connections). E.g. '/MyContext/_MyWorkspace/_plugins/MyPlugin'
*/
getPluginDirectPrefix: function (plugin) { return Ametys.WORKSPACE_URI + Ametys.PLUGINS_DIRECT_PREFIX + '/' + plugin;},
/**
* Get the url prefix for wrapped connection to a plugin (e.g. for redirections to this plugin)
* @param {String} plugin The plugin name. Cannot be null or empty.
* @return {String} The url prefix for accessing plugins for displaying (e.g. to display a page rendered by the plugin). E.g. '/MyContext/_MyWorkspace/plugins/MyPlugin'
*/
getPluginWrapperPrefix: function(plugin) { return Ametys.WORKSPACE_URI + Ametys.PLUGINS_WRAPPED_PREFIX + '/' + plugin;},
/**
* Get the url prefix for downloading resource file of a plugin. The use of Ametys.getPluginResourcesPrefix('test') + '/img/image.png' will return '/MyContext/plugins/test/resources/img/image.png'
* @param {String} plugin The plugin name. Cannot be null or empty.
* @return {String} The url prefix for accessing plugin resources (e.g. js, css or images files). E.g. '/MyContext/plugins/MyPlugin/resources'
*/
getPluginResourcesPrefix: function (plugin) { return Ametys.CONTEXT_PATH + "/plugins/" + plugin + "/resources"; },
/**
* Get the url prefix for accessing a workspace resource file. The use of Ametys.getWorkspaceResourcesPrefix('wsp') + '/img/image.png' will return '/MyContext/_wsp/resources/img/image.png'
* @param {String} workspace The workspace name. If omitted, the current workspace will be used.
* @return {String} The url prefix for accessing workspace resources (e.g. js, css or images files). E.g. '/MyContext/_wsp/resources'
*/
getWorkspaceResourcesPrefix: function(workspace)
{
if (workspace == null)
{
return Ametys.WORKSPACE_URI + '/resources';
}
else
{
return Ametys.CONTEXT_PATH + '/_' + workspace + '/resources';
}
},
/**
* Convert html tags to textare.
* @param {String} s The string to convert containing text with some br tags
* @return {String} The textare compliant string with 0x13 characters
*/
convertHtmlToTextarea: function(s)
{
s = s.replace(/<br\/>/g, "\r\n");
s = s.replace(/"/g, "\"");
s = s.replace(/'/g, "'");
s = s.replace(/</g, "<");
s = s.replace(/>/g, ">");
s = s.replace(/&/g, "&");
return s;
},
/**
* The opposite of {@link #convertHtmlToTextarea}
* @param {String} s The string to convert containing text with some 0x13 characters
* @return {String} The html compliant string with br tags
*/
convertTextareaToHtml: function (s)
{
s = s.replace(/\r?\n/g, "<br/>");
s = s.replace(/"/g, """);
s = s.replace(/'/g, "'");
return s;
},
/**
* Load a css file
* @param {String} url The url of the css file to load
* @param {String} [media] The css media to set. Such as 'screen', 'print'... or a comma separated list.
*/
loadStyle: function (url, media)
{
var head = document.getElementsByTagName("head")[0];
var link = document.createElement("link");
link.rel = "stylesheet";
link.href = url;
link.type = "text/css";
if (media)
{
link.media = media;
}
head.appendChild(link);
},
/**
* Load a js file
* @param {String} url The url of the js file to load
* @param {Function} onLoad The callback to call on successful load.
* @param {Function} onError The callback to call on failure to load.
*/
loadScript: function (url, onLoad, onError)
{
var me = this;
function internalOnError (msg)
{
var message = "{{i18n PLUGINS_CORE_UI_LOADSCRIPT_ERROR}}" + url;
me.getLogger().error(message);
if (Ext.isFunction (onError))
{
onError.call (null, msg);
}
}
function internalOnLoad (msg)
{
if (me.getLogger().isInfoEnabled())
{
var message = "{{i18n PLUGINS_CORE_UI_LOADSCRIPT_SUCCESS}}" + url;
me.getLogger().info(message);
}
if (Ext.isFunction (onLoad))
{
onLoad.call ();
}
}
// Disable cache to avoid URL ends with '?_dc=1379928434854'
Ext.Loader.setConfig ({
disableCaching : false
});
Ext.Loader.loadScript({
url: url,
onLoad: internalOnLoad,
onError: internalOnError
});
Ext.Loader.setConfig ({
disableCaching : true
});
},
/**
* Retrieve an object by its name
* @param {String} name The name of the object
* @param {Object} [context=window] The search context (relative to the object name).
* @return {Object} The desired object, or null if it does not exist.
*/
getObjectByName: function(name, context)
{
context = context || window;
var namespaces = name.split(".");
var obj = context;
var prop;
for (var i = 0; i < namespaces.length; i++)
{
prop = namespaces[i];
if (prop in obj)
{
obj = obj[prop];
}
else
{
return null;
}
}
return obj;
},
/**
* Create an object by supplying its constructor name
* @param {String} name The name of the constructor
* @param {Object} [context=window] The search context (relative to the constructor name).
* @param {Object...} [args] Optional function arguments.
* @return {Object} The object returned by the constructor call.
*/
createObjectByName: function(name, context/*, args*/)
{
var fn = Ametys.getObjectByName(name, context);
if (!fn) return null; // return null if fn is falsy.
var args = Array.prototype.slice.call(arguments, 2);
// new operator must be call on a wrapped function like this one.
var wrappedCtor = function(args)
{
var ctor = function() {
// Here 'this' is set to the new keyword.
fn.apply(this, args);
}
// Inherits the prototype.
ctor.prototype = fn.prototype;
return ctor;
};
var ctor = wrappedCtor(args);
return new ctor();
},
/**
* Get a function by supplying its name
* @param {String} name The name of the object
* @param {Object} [context=window] The search context (relative to the object name).
* @param {Object} [scope=context] The scope for the function call.
* @return {Object} The function, or null if the function does not exist.
*/
getFunctionByName: function(name, context, scope)
{
var ctx = context || window;
var namespaces = name.split(".");
var prop;
for (var i = 0; i < namespaces.length-1; i++)
{
prop = namespaces[i];
if (prop in ctx)
{
ctx = ctx[prop];
}
else
{
return null;
}
}
var fn = ctx[namespaces.pop()];
if (!fn) return null; // return null if fn is falsy.
var scp = scope || context || ctx;
return Ext.bind(fn, scp);
},
/**
* Executes a function by supplying its name
* @param {String} name The name of the object
* @param {Object} [context=window] The search context (relative to the object name).
* @param {Object} [scope=context] The scope for the function call.
* @param {Object...} [args] Optional function arguments.
* @return {Object} The result of the function
* @throws an error if the function is undefined
*/
executeFunctionByName: function(name, context, scope/*, args*/)
{
var fn = Ametys.getFunctionByName(name, context, scope);
if (fn == null)
{
var message = "Can not execute undefined function '" + name + "'.";
this.getLogger().error(message);
throw new Error (message);
}
var args = Array.prototype.slice.call(arguments, 3);
return fn.apply(null, args);
},
/**
* Open a popup window with http POST or GET data
* @param {String} url the window url
* @param {Object} [data] the request parameters
* @param {String} [method="POST"] the request method for data, 'GET' or 'POST', defaults to "POST"
* @param {String} [target="_blank"] an optional opening target (a name, or "_self"), defaults to "_blank"
*/
openWindow: function (url, data, method, target)
{
var form = document.createElement("form");
form.action = url;
form.method = method || "POST";
form.target = target || "_blank";
if (data)
{
for (var key in data)
{
var values = Ext.Array.from(data[key]);
Ext.Array.each(values, function (value) {
var input = document.createElement("input");
input.type = 'hidden';
input.name = key;
input.value = typeof value === "object" ? Ext.JSON.encode(value) : value;
form.appendChild(input);
})
}
}
form.style.position = "absolute";
form.style.left = "-10000px";
document.body.appendChild(form);
try
{
form.submit();
}
catch(e)
{
// On Chrome, this code is not reached in case of blocked pop-up
Ametys.MessageBox.alert("{{i18n PLUGINS_CORE_UI_OPEN_WINDOW_POPUP_BLOCKED_TITLE}}", "{{i18n PLUGINS_CORE_UI_OPEN_WINDOW_POPUP_BLOCKED}}");
}
// delete form
window.setTimeout(function () {document.body.removeChild(form)}, 1);
},
/**
* Is Ametys suspended?
* @return {boolean} true if suspended
*/
isSuspended: function()
{
return this._off === true;
},
/**
* Suspend the application and display a message. A restart link is automatically added. An optionnal continue link<br/>
* @param {String} [title] A title
* @param {String} [message] A html message to display.
* @param {String} [action] A text message to set on the action button.
* @param {Boolean} [displayContinueMessage] A text message to set on the continue action button.
* @param {Boolean} [tryToRelogOnContinue] If true and displayContinueMessage is true, clicking on the continue message will also try to relog
*/
suspend: function(title, message, action, displayContinueMessage, tryToRelogOnContinue)
{
if (Ametys._off == true)
{
return;
}
Ametys._off = true;
Ametys.data.ServerComm.suspend();
this._suspend(title, message, action, displayContinueMessage, tryToRelogOnContinue)
if (window.console)
{
console.info("Ametys suspended");
}
},
/**
* @private
* Display the suspend message. A restart link is automatically added. An optionnal continue link<br/>
* @param {String} [title] A title
* @param {String} [message] A html message to display.
* @param {String} [action] A text message to set on the action button.
* @param {Boolean} [displayContinueMessage] A text message to set on the continue action button.
* @param {Boolean} [tryToRelogOnContinue] If true and displayContinueMessage is true, clicking on the continue message will also try to relog
*/
_suspend: function(title, message, action, displayContinueMessage, tryToRelogOnContinue)
{
action = action || "{{i18n PLUGINS_CORE_UI_SHUTDOWN_DEFAULTACTION}}";
Ext.getBody().getParent().addCls("offline");
if (title)
{
var wrapper = document.createElement('div');
wrapper.setAttribute("id", "ametys-offline");
wrapper.setAttribute("class", "offline");
wrapper.innerHTML = "<div>"
+ "<div class='offline-title'></div>"
+ "<div class='offline-message'></div>"
+ "<div class='offline-buttons'>"
+ "<a id='ametys-offline-2'>" + action + "</a>"
+ (displayContinueMessage ? "<a id='ametys-offline-1'>{{i18n PLUGINS_CORE_UI_SHUTDOWN_ALTERNATIVEACTION}}</a>" : "")
+ "</div>"
+ "</div>";
document.body.appendChild(wrapper);
let offlineDiv = Ext.fly(Ext.getBody().selectNode("#ametys-offline"));
offlineDiv.select(".offline-title").setHtml(title)
offlineDiv.select(".offline-message").setHtml(message)
if (displayContinueMessage)
{
Ext.get("ametys-offline-1").on('click', function() {
Ametys._off = false;
Ext.get("ametys-offline-1").destroy();
Ext.get("ametys-offline-2").destroy();
offlineDiv.remove();
Ext.getBody().getParent().removeCls("offline");
Ametys.data.ServerComm.restart();
if (tryToRelogOnContinue)
{
Ametys.data.ServerComm._relog(false);
}
}, null, {single: true});
}
Ext.get("ametys-offline-2").on('click', function() {
offlineDiv.remove();
Ametys.reload();
}, null, {single: true});
}
},
/**
* @private
* Get out from the suspend mode
*/
_unsuspend: function()
{
Ametys._off = false;
let offline1 = Ext.get("ametys-offline-1");
if (offline1)
{
offline1.destroy();
}
let offline2 = Ext.get("ametys-offline-2");
if (offline2)
{
offline2.destroy();
}
let offlineDiv = Ext.fly(Ext.getBody().selectNode("#ametys-offline"));
if (offlineDiv)
{
offlineDiv.remove();
}
Ext.getBody().getParent().removeCls("offline");
Ametys.data.ServerComm.restart();
},
/**
* Reload the application. Current parameters will be replaced. A 'foo' parameter will be added to avoid cache.
* @param {String} [params] The params to add to the url. e.g. "myparam=value1&myotherparam=value2"
*/
reload: function(params)
{
// Shutdown request
Ametys.data.ServerComm.suspend();
// Close error dialogs
Ametys.log.ErrorDialog._okMessages();
Ametys.log.ErrorDialog = { display: function() {}, _okMessages: function() {} };
// Compute new url
var href = window.location.href;
var i1 = href.indexOf('?');
if (i1 >= 0)
{
href = href.substring(0, i1);
}
var i2 = href.indexOf('#');
if (i2 >= 0)
{
href = href.substring(0, i2);
}
var i3 = href.indexOf(';');
if (i3 >= 0)
{
href = href.substring(0, i3);
}
// open new url
window.location.href = href + "?" + (params || "") + "&foo=" + Math.random();
},
/**
* Close the application by exiting
* If an app param "back-url" is available, current window will be redirected to it (String).
* Else If an app param "callback" is available, it will be called (Function).
* Else the window will be closed
*/
exit: function()
{
if (this.getAppParameter('back-url') != null)
{
window.location.href = this.getEnvParams('back-url');
}
if (typeof(this.getAppParameter('callback')) == 'function')
{
this.getEnvParams('callback')();
}
else
{
window.close();
}
},
/**
* Is the browser unloading the window
* @return {Boolean} True if unloading
*/
isUnloading: function()
{
return Ametys._unloading === true;
},
/**
* @private
* Listener when the window is unloading
*/
_onUnload: function()
{
Ametys._unloading = true;
},
/**
* Open the given tool in given workspace
* @param {String} workspaceName The workspace name. Can be null for default
* @param {String} toolId The tool id
* @param {Object} params The params for the tool. Becareful, the current implementation does not encode params correctly yet.
*/
openWorkspaceTool: function(workspaceName, toolId, params)
{
var url = window.location.origin + Ametys.CONTEXT_PATH + "/" + (workspaceName ? "_" + workspaceName : "") + "/index.html?uitool=" + toolId;
var paramsAsString = "{";
var first = true;
for (let key in params)
{
url += "," + key + ":%27" + params[key] + "%27";
paramsAsString += (first ? "" : ",") + key + ":\"" + params[key] + "\"";
first = false;
}
paramsAsString += "}";
var requestJS = "try{if (Ametys.isSuspended()){throw new Error();};if (Ametys.tool.ToolsManager.getFactory('" + toolId + "')) {void(Ametys.tool.ToolsManager.openTool('" + toolId + "'," + paramsAsString + "));} else {throw new Error();}} catch(e) {window.location.href =\"" + url + "\"}";
window.open("javascript:" + requestJS, workspaceName || "default");
}
}
);
/*
* Initialize Ext.Date with suitable patterns
*/
/**
* @member Ext.Date
* @property {Object} patterns Predefined internationalized patterns
* @property {String} patterns.ISO8601DateTime Date using the ISO 8601 standard for date and time. E.g. 2013-02-28T17:13:50.000Z
* @property {String} patterns.ISO8601Date Simplier format of #patterns.ISO8601Date with date only date. E.g 2013-02-28
* @property {String} patterns.ShortDate A short date depending on language. Can be 02/28/13 in english.
* @property {String} patterns.LongDate A long date depending on language. Can be 02/28/2013 in english.
* @property {String} patterns.FullDate A full readable date depending on language. Can be Thursday, February 2, 2013 in english.
* @property {String} patterns.VeryShortTime A short time depending on language. Can be 03:15 AM in english.
* @property {String} patterns.ShortTime A short time depending on language. Can be 03:15:50 AM in english.
* @property {String} patterns.LongTime A long time depending on language. Can be 03:15:35+02:00 AM in english.
* @property {String} patterns.ShortDateTime A combinaison of #patterns.ShortDate and #patterns.ShortTime. Can be 02/28/13 03:15:50 AM in english.
* @property {String} patterns.FriendlyDateTime A readable #patterns.LongDate, a keyword (such as 'at') and very short time. Can be 02/28/2013 at 03:15 AM in english.
* @property {String} patterns.LongDateTime A combinaison of #patterns.LongDate and #patterns.LongTime. Can be 02/28/2013 03:15:35+02:00 AM in english.
* @property {String} patterns.FullDateTime A combinaison of #patterns.FullDate, a keyword (such as 'at') and a long readable time. Can be Thursday, February 28, 2013 at 05:13:50 GMT PM in english.
* @since Ametys-Runtime-3.9
* @ametys
*/
Ext.Date.patterns = {
ISO8601DateTime:"C",
ISO8601Date:"Y-m-d",
ShortDate: "{{i18n PLUGINS_CORE_UI_DATETIME_SHORTDATE}}",
LongDate: "{{i18n PLUGINS_CORE_UI_DATETIME_LONGDATE}}",
FullDate: "{{i18n PLUGINS_CORE_UI_DATETIME_FULLDATE}}",
VeryShortTime: "{{i18n PLUGINS_CORE_UI_DATETIME_VERYSHORTTIME}}",
ShortTime: "{{i18n PLUGINS_CORE_UI_DATETIME_SHORTTIME}}",
LongTime: "{{i18n PLUGINS_CORE_UI_DATETIME_LONGTIME}}",
ShortDateTime: "{{i18n PLUGINS_CORE_UI_DATETIME_SHORTDATETIME}}",
FriendlyDateTime: "{{i18n PLUGINS_CORE_UI_DATETIME_FRIENDLYDATETIME}}",
LongDateTime: "{{i18n PLUGINS_CORE_UI_DATETIME_LONGDATETIME}}",
FullDateTime: "{{i18n PLUGINS_CORE_UI_DATETIME_FULLDATETIME}}"
};
/*
* Changing default ajax method to POST and timeout to a long time
*/
Ext.Ajax.setTimeout(365*24*60*60*1000);
Ext.Ajax.setMethod('POST');
/*
* Load localization for extjs
*/
(function ()
{
// This is to change default value
Ext.define("Ext.locale.i18n.LoadMask", {
override: "Ext.LoadMask",
msg: "{{i18n PLUGINS_CORE_UI_LOADMASK_DEFAULT_MESSAGE}}"
});
})();
/*
* Remove ametys_opts object
*/
window.ametys_opts = undefined;
Ext.get(window).on('unload', Ametys._onUnload);