import { checkBrowserCompatibility } from '../../view/helpers';
import { Events, ApiMethods, callApiMethod, parseJson, _sessionList } from './components';
import { PromptLogin } from './promptlogin';
import { Resources } from '../../resources/resources';
let PmaStartUrl = "http://127.0.0.1:54001/";
export
/**
* The Context class glues component instances together. It provides API method implementations and simplifies the interaction with PMA.core by automatically managing authentication and sessionID handling, via the authentication provider classes.
* @memberof PMA.UI.Components
* @alias Context
* @class
* @param {Object} options
* @param {string} options.caller
* @tutorial 03-gallery
* @tutorial 04-tree
* @tutorial 05-annotations
*/
class Context {
constructor(options) {
if (!checkBrowserCompatibility()) {
return;
}
if (!options || typeof options.caller !== "string") {
throw "Caller parameter not supplied";
}
this.options = options;
// create event listeners object and add one array for each event type
this.listeners = {};
for (var ev in Events) {
// if (Events.hasOwnProperty(ev)) {
if (Object.prototype.hasOwnProperty.call(Events, ev)) {
this.listeners[Events[ev]] = [];
}
}
// list of components that can authenticate against a PMA.core server
this.authenticationProviders = [];
}
/**
* Gets the caller value
* @return {string}
*/
getCaller() {
return this.options.caller;
}
/**
* Adds an authentication provider to the list of available authentication methods
* @param {AutoLogin|PromptLogin|SessionLogin} provider
*/
registerAuthenticationProvider(provider) {
if (typeof provider.authenticate !== "function") {
console.error("Invalid authentication provider");
}
else {
this.authenticationProviders.push(provider);
}
}
/**
* Removes an authentication provider from the list of available authentication methods
* @param {AutoLogin|PromptLogin|SessionLogin} provider - The provider to remove
* @param {bool} clearCache - Clears the session ids cache
*/
removeAuthenticationProvider(provider, clearCache) {
if (typeof provider.authenticate !== "function") {
console.error("Invalid authentication provider");
}
else {
for (var i = 0; i < this.authenticationProviders.length; i++) {
if (this.authenticationProviders[i] === provider) {
this.authenticationProviders.splice(i, 1);
// Clear all cache
if (clearCache !== false) {
_sessionList.clear();
}
break;
}
}
}
}
/**
* Gets the list of available authentication methods
* @returns [{PMA.UI.Authentication.AutoLogin|PMA.UI.Authentication.PromptLogin|PMA.UI.Authentication.SessionLogin}] providers
*/
getAuthenticationProviders() {
return this.authenticationProviders;
}
/**
* PMA.core authentication response
* @typedef {Object} Context~authenticationResponse
* @property {string} SessionId - The session ID
* @property {string} Username - The username
* @property {string} Email - The user's email
* @property {string} FirstName - The user's first name
* @property {string} LastName - The user's last name
*/
/**
* A function called after successfully pinging a list of servers
* @callback Context~pingServersDoneCallback
* @param {String[]} servers - A sorted list of server url's from fastest to slowest
* @param {Object[]} detailInfo - An array of detailed information from pinging the servers (not sorted)
* @param {string} detailInfo.serverUrl - The server url
* @param {bool} detailInfo.success - Whether the server responded to any pinging
* @param {Number[]} detailInfo.times - An array of all the times the server took to respond (in miliseconds)
* @param {Number} detailInfo.avgTime - The average time the server took to respond (in miliseconds)
* @param {Number} detailInfo.attempts - The number of attempted pings to the server
*/
/**
* Pings a list of servers to find the fastests
* @param {string[]} servers - An array of server url to ping
* @param {Context~pingServersDoneCallback} done - The done callback to run
* @param {Number} [maxAttempts=5] - The number of attempts for each server
* */
pingServers(servers, done, maxAttempts) {
if (maxAttempts <= 1 || !maxAttempts || maxAttempts === undefined) {
maxAttempts = 6;
}
var instances = servers.map(function (s) { return { serverUrl: s, success: false, times: [], avgTime: null, attempts: 0 }; });
var cb = function (startTime, instanceIndex, success) {
instances[instanceIndex].attempts++;
if (success && instances[instanceIndex].attempts > 1) {
// We ignore first attempt for warm up
var time = performance.now() - startTime;
instances[instanceIndex].times.push(time);
instances[instanceIndex].avgTime = instances[instanceIndex].avgTime != null ?
((instances[instanceIndex].avgTime * instances[instanceIndex].times.length) + time) / (instances[instanceIndex].times.length + 1) :
time;
instances[instanceIndex].success = true;
}
if (instances[instanceIndex].attempts >= maxAttempts) {
instanceIndex++;
}
if (instanceIndex >= instances.length) {
// done testing
if (typeof done === "function") {
done(instances.sort(function (a, b) {
if (a.avgTime != null && b.avgTime != null) {
return a.avgTime - b.avgTime;
}
else if (a.avgTime != null) {
return -10000000;
}
else {
return 10000000;
}
}).map(function (s) { return s.serverUrl; }), instances);
}
}
else {
// Run next test
var nowTime = performance.now();
callApiMethod({
serverUrl: instances[instanceIndex].serverUrl,
method: ApiMethods.GetVersionInfo,
success: cb.bind(this, nowTime, instanceIndex, true),
failure: cb.bind(this, nowTime, instanceIndex, false)
});
}
};
//Start first test
var nowTime = performance.now();
callApiMethod({
serverUrl: instances[0].serverUrl,
method: ApiMethods.GetVersionInfo,
success: cb.bind(this, nowTime, 0, true),
failure: cb.bind(this, nowTime, 0, false)
});
}
/**
* Connection information
* @typedef {Object} Context~connection
* @property {Number} roundTrip - The ping time in milliseconds
* @property {Number} downloadSpeed - The download speed in bytes/second
*/
/**
* Calculates the ping time and download speed between the client and a PMA.core instance
* @param {string} serverUrl - The server url to connect to
* @returns {Context~connection}
**/
async evaluateConnection(serverUrl) {
if (!serverUrl.endsWith('/')) {
serverUrl += '/';
}
const maxAttempts = 10;
const dataSize = 50 * 1024 * 1024;
let pingTime = 0;
let i = 0;
do {
let start = performance.now();
try {
await fetch(`${serverUrl}api/json/GetVersionInfo`);
}
catch {
console.error("connection failed");
return {
roundTrip: null,
downloadSpeed: null
};
}
let end = performance.now();
pingTime += end - start;
i++;
} while (i < maxAttempts)
pingTime /= maxAttempts;
let start = performance.now();
try {
const resp = await fetch(`${serverUrl}speedtest`, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
"body": `size=${dataSize}`,
"method": "POST",
// "mode": 'no-cors'
});
const reader = resp.body.getReader();
var finalDone = false;
while (!finalDone) {
const { done, value } = await reader.read();
finalDone = done;
}
}
catch {
console.error("download failed");
return {
roundTrip: pingTime,
downloadSpeed: null
};
}
let end = performance.now();
const downloadTime = end - start;
const transferSpeed = dataSize / (downloadTime / 1000);
return {
roundTrip: pingTime,
downloadSpeed: transferSpeed
};
}
/**
* Gets the user information associated with a server
* @param {string} serverUrl - The URL of the PMA.core for which to fetch user information
* @returns {Context~authenticationResponse} If no authentication has taken place for the particular server, null is returned, otherwise an object.
*/
getUserInfo(serverUrl) {
for (var url in _sessionList.get()) {
// if (_sessionList.get().hasOwnProperty(url) && url === serverUrl && _sessionList.get()[url]) {
if (Object.prototype.hasOwnProperty.call(_sessionList.get(), url) && url === serverUrl && _sessionList.get()[url]) {
return _sessionList.get()[url];
}
}
return null;
}
/**
* Called when a session ID was successfully obtained
* @callback Context~getSessionCallback
* @param {string} sessionID
*/
/**
* Finds a session ID for the requested server, either by scanning the already cached session ids or by invoking one by one the available authentication providers, until a valid session ID is found
* @param {string} serverUrl - The URL of the PMA.core for which to fetch a session ID
* @param {Context~getSessionCallback} success
* @param {function} [failure]
*/
getSession(serverUrl, success, failure) {
// scan cached session IDs
for (var url in _sessionList.get()) {
// if (_sessionList.get().hasOwnProperty(url) && url === serverUrl && _sessionList.get()[url]) {
if (Object.prototype.hasOwnProperty.call(_sessionList.get(), url) && url === serverUrl && _sessionList.get()[url]) {
success(_sessionList.get()[url].SessionId);
return;
}
}
// no session ID found in cached list
// fire all providers until one succeeds or all failed
authenticateWithProvider.call(this, serverUrl, success, failure);
}
getImageInfo(serverUrl, pathOrUid, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.GetImageInfo,
data: {
pathOrUid: pathOrUid
},
httpMethod: "GET",
success: success,
failure: failure
});
}
getFiles(serverUrl, path, success, failure) {
console.warn("Context.getFiles is deprecated please use Context.getSlides instead");
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.GetFiles,
data: {
path: path
},
httpMethod: "GET",
success: success,
failure: failure
});
}
/**
* Gets all slides in a specified path
* @param {Object} options - Parameters to pass to the GetSlides request
* @param {string} options.serverUrl - The server url to get slides from
* @param {string} options.path - The path to get slides from
* @param {PMA.UI.Components.GetSlidesScope} options.scope - The search scope to use
* @param {function} [options.success] - Called upon success
* @param {function} [options.failure] - Called upon failure
**/
getSlides(options) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: options.serverUrl,
method: ApiMethods.GetFiles,
data: {
path: options.path,
scope: options.scope ? options.scope : 0
},
httpMethod: "GET",
success: options.success,
failure: options.failure
});
}
getDirectories(serverUrl, path, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.GetDirectories,
data: {
path: path
},
httpMethod: "GET",
success: success,
failure: failure
});
}
createDirectory(serverUrl, path, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.CreateDirectory,
data: {
path: path
},
httpMethod: "POST",
contentType: "application/json",
success: success,
failure: failure
});
}
deleteDirectory(serverUrl, path, deleteContents, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.DeleteDirectory,
data: {
path: path,
deleteContents: deleteContents
},
httpMethod: "POST",
contentType: "application/json",
success: success,
failure: failure
});
}
renameDirectory(serverUrl, path, newName, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.RenameDirectory,
data: {
path: path,
newName: newName
},
httpMethod: "POST",
contentType: "application/json",
success: success,
failure: failure
});
}
deleteSlide(serverUrl, path, deleteAsFile, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.DeleteSlide,
data: {
path: path,
deleteAsFile: deleteAsFile
},
httpMethod: "POST",
contentType: "application/json",
success: success,
failure: failure
});
}
getAnnotations(serverUrl, path, currentUserOnly, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.GetAnnotations,
data: {
pathOrUid: path,
currentUserOnly: currentUserOnly
},
httpMethod: "GET",
success: success,
failure: failure
});
}
addAnnotation(serverUrl, path, classification, layerID, notes, geometry, color, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.AddAnnotation,
data: {
pathOrUid: path,
classification: classification,
layerID: layerID,
notes: notes,
geometry: geometry,
color: color
},
httpMethod: "POST",
contentType: "application/json",
success: success,
failure: failure
});
}
updateAnnotation(serverUrl, path, layerID, annotationID, notes, geometry, color, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.UpdateAnnotation,
data: {
pathOrUid: path,
layerID: layerID,
annotationID: annotationID,
notes: notes,
geometry: geometry,
color: color
},
httpMethod: "POST",
contentType: "application/json",
success: success,
failure: failure
});
}
saveAnnotations(serverUrl, path, added, updated, deleted, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.SaveAnnotations,
data: {
pathOrUid: path,
added: added,
updated: updated,
deleted: deleted
},
httpMethod: "POST",
contentType: "application/json",
success: success,
failure: failure
});
}
deleteAnnotation(serverUrl, path, layerID, annotationID, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.DeleteAnnotation,
data: {
pathOrUid: path,
layerID: layerID,
annotationID: annotationID
},
httpMethod: "POST",
contentType: "application/json",
success: success,
failure: failure
});
}
getFormDefinitions(serverUrl, formIDs, rootDirectoryAlias, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.GetFormDefinitions,
data: {
formIDs: formIDs instanceof Array ? formIDs.join(",") : formIDs,
rootDirectoryAlias: rootDirectoryAlias
},
httpMethod: "GET",
success: success,
failure: failure
});
}
getFormSubmissions(serverUrl, pathOrUids, formIDs, currentUserOnly, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.GetFormSubmissions,
data: {
pathOrUids: pathOrUids instanceof Array ? pathOrUids : [],
formIDs: formIDs instanceof Array ? formIDs : [],
currentUserOnly: currentUserOnly
},
httpMethod: "POST",
contentType: "application/json",
success: success,
failure: failure
});
}
saveFormDefinition(serverUrl, definition, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.SaveFormDefinition,
apiPath: "admin",
contentType: "application/json",
data: {
definition: definition
},
httpMethod: "POST",
success: success,
failure: failure
});
}
deleteFormDefinition(serverUrl, formID, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.DeleteFormDefinition,
apiPath: "admin",
data: {
formID: formID
},
httpMethod: "GET",
success: success,
failure: failure
});
}
getVersionInfo(serverUrl, success, failure) {
callApiMethod({
serverUrl: serverUrl,
method: ApiMethods.GetVersionInfo,
success: function (http) {
if (typeof success === "function") {
var response = parseJson(http.responseText);
success(response);
}
},
failure: function (http) {
if (typeof failure === "function") {
if (http.responseText && http.responseText.length !== 0) {
var response = parseJson(http.responseText);
failure(response);
}
else {
failure({ Message: Resources.translate("Get Version Info failed") });
}
}
}
});
}
/**
* Gets the events log from the server
* @param {Object} options - Parameters to pass to the GetEvents request
* @param {string} options.serverUrl - The server url to get events log
* @param {number} options.page - The page to fetch
* @param {number} options.pageSize - The page size to fetch
* @param {function} [options.success] - Called upon success
* @param {function} [options.failure] - Called upon failure
**/
getEvents(options, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: options.serverUrl,
method: ApiMethods.GetEvents,
apiPath: "admin",
data: {
page: options.page,
pageSize: options.pageSize
},
httpMethod: "GET",
success: success,
failure: failure
});
}
deAuthenticate(serverUrl, success, failure) {
var sessionId = null;
// if (_sessionList.get().hasOwnProperty(serverUrl) && _sessionList.get()[serverUrl]) {
if (Object.prototype.hasOwnProperty.call(_sessionList.get(), serverUrl) && _sessionList.get()[serverUrl]) {
sessionId = _sessionList.get()[serverUrl].SessionId;
if (!sessionId) {
// nothing to do, no session ID cached, call success
if (typeof success === "function") {
success();
return;
}
}
}
else {
var cbFn = function (sId) {
sessionId = sId;
};
// there is no cached sessionId for this server check for a sessionLogin provider
for (var i = 0; i < this.authenticationProviders.length; i++) {
if (this.authenticationProviders[i] instanceof PMA.UI.Authentication.SessionLogin) { // eslint-disable-line no-undef
// check that this sessionLogin provider can handle the requested serverUrl
if (this.authenticationProviders[i].authenticate(serverUrl, cbFn, null)) {
break;
}
continue;
}
}
}
if (sessionId == null) {
// cannot handle this serverUrl just return
return;
}
_sessionList.set(serverUrl, null);
callApiMethod({
serverUrl: serverUrl,
method: ApiMethods.DeAuthenticate,
data: { sessionID: sessionId },
success: function () {
if (typeof success === "function") {
success();
}
},
failure: function (http) {
if (typeof failure === "function") {
if (http.responseText && http.responseText.length !== 0) {
var response = parseJson(http.responseText);
failure(response);
}
else {
failure();
}
}
}
});
}
queryFilename(serverUrl, path, pattern, success, failure) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: serverUrl,
method: ApiMethods.QueryFilename,
apiPath: "query",
data: {
path: path,
pattern: pattern
},
httpMethod: "GET",
success: success,
failure: failure
});
}
/**
* Gets all distinct values for a field in a form
* @param {Object} options - Parameters to pass to the "distinct values" request
* @param {string} options.serverUrl - The server url to use
* @param {string} options.formId - The Form Id to use
* @param {string} options.fieldId - The Field Id to get distinct values for
* @param {function} [options.success] - Called upon success
* @param {function} [options.failure] - Called upon failure
**/
distinctValues(options) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: options.serverUrl,
method: ApiMethods.DistinctValues,
apiPath: "query",
data: {
formID: options.formId,
fieldID: options.fieldId
},
httpMethod: "GET",
success: options.success,
failure: options.failure
});
}
/**
* Gets information for all slides specified
* @param {Object} options - Parameters to pass to the GetSlides request
* @param {string} options.serverUrl - The server url to get slides from
* @param {string[]} options.images - An array of image paths or uids to fetch information for
* @param {function} [options.success] - Called upon success
* @param {function} [options.failure] - Called upon failure
**/
getImagesInfo(options) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: options.serverUrl,
method: ApiMethods.GetImagesInfo,
apiPath: "api",
data: {
pathOrUids: options.images
},
contentType: "application/json",
httpMethod: "POST",
success: options.success,
failure: options.failure
});
}
/**
* Gets slides that satisfy the specified expressions
* @param {Object} options - Parameters to pass to the "distinct values" request
* @param {string} options.serverUrl - The server url to use
* @param {Object[]} options.expressions - The Expressions to use
* @param {Number} options.expressions.FormID - The form Id for this expression
* @param {Number} options.expressions.FieldID - The field Id for this expression
* @param {Number} options.expressions.Operator - The Operator for this expression ( Equals = 0, LessThan = 1, LessThanOrEquals = 2, GreaterThan = 3, GreaterThanOrEquals = 4)
* @param {Number} options.expressions.Value - The value to compare for this expression
* @param {function} [options.success] - Called upon success
* @param {function} [options.failure] - Called upon failure
**/
metadata(options) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: options.serverUrl,
method: ApiMethods.Metadata,
apiPath: "query",
data: {
expressions: options.expressions
},
httpMethod: "POST",
contentType: "application/json",
success: options.success,
failure: options.failure
});
}
/**
* Checks if a PMA.start server is available
* @param {function} success - The function to call when check succeded
* @param {function} failure - The function to call when check failed
**/
checkPmaStartServer(success, failure) {
callApiMethod({
method: ApiMethods.GetVersionInfo,
httpMethod: "GET",
serverUrl: PmaStartUrl,
success: function () {
if (typeof callback === "function") {
success.call(this, true);
}
},
failure: function () {
if (typeof callback === "function") {
failure.call(this, false);
}
}
});
}
/**
* Returns the url to PMA.start CORS allow page
* @return {string}
**/
getPmaStartCorsUrl() {
var url = PmaStartUrl + "home/allowdomain/?host=" + document.location.origin;
return url;
}
/**
* Gets slides that satisfy the specified expressions
* @param {Object} options - Parameters to pass to the "distinct values" request
* @param {string} options.serverUrl - The server url to use
* @param {string} options.name - The script name to run
* @param {object} [options.params] - Optional script parameters to pass
* @param {function} [options.success] - Called upon success
* @param {function} [options.failure] - Called upon failure
**/
runScript(options) {
callApiMethodWithAuthentication.call(
this, {
attemptCount: 0,
serverUrl: options.serverUrl,
method: ApiMethods.RunScripts,
apiPath: "scripts",
webapi: true,
data: Object.assign({
name: options.name,
}, options.params),
httpMethod: "GET",
success: options.success,
failure: options.failure
});
}
// registers an event listener
listen(eventName, callback) {
// if (!this.listeners.hasOwnProperty(eventName)) {
if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
console.error(eventName + " is not a valid event");
}
this.listeners[eventName].push(callback);
}
fireEvent(eventName, eventArgs) {
// if (!this.listeners.hasOwnProperty(eventName)) {
if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
console.error(eventName + " does not exist");
return;
}
for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
this.listeners[eventName][i](eventArgs);
}
}
/**
* A function called for progress on an upload
* @callback Context~uploadProgressCallback
* @param {String} name - The file name uploaded
* @param {Number} progress - The progress percentage
*/
/**
* Uploads a file to the pma.core server
* @param {Object} options
* @param {string} options.serverUrl - The server url to use
* @param {string} options.targetPath - The virtual path to upload to
* @param {File} options.file - The file object to upload
* @param {Context~uploadProgressCallback} options.progress - A progress callback
* @returns A promise to get the result of the upload
*/
uploadFile(options) {
let that = this;
return new Promise(function (resolve, reject) {
callApiMethodWithAuthentication.call(
that, {
attemptCount: 0,
serverUrl: options.serverUrl,
method: ApiMethods.Upload,
apiPath: "transfer",
webapi: true,
data: {
"path": options.targetPath,
"files": [{
"isMain": true,
"length": options.file.size,
"path": options.file.name
}]
},
httpMethod: "POST",
contentType: "application/json",
success: function (sessionId, uploadContent) {
let method = "PUT";
if (!uploadContent.Urls) {
uploadContent.Urls = [];
}
if (uploadContent.Urls.length === 0) {
method = "POST";
uploadContent.Urls.push(`${options.serverUrl}transfer/Upload/${uploadContent.Id}?sessionId=${sessionId}&path=${encodeURIComponent(options.file.name)}`);
}
var fd = new FormData();
fd.append("file", options.file);
var xhr = new XMLHttpRequest();
xhr.open(method, uploadContent.Urls[0], true);
xhr.upload.onprogress = function (e) {
let progress = Math.ceil((e.loaded / e.total) * 100);
if (typeof options.progress === "function") {
options.progress(options.file.name, progress);
}
};
xhr.onload = function () {
resolve(true);
};
xhr.onerror = function () {
reject();
}
if (method === "POST") {
xhr.send(fd);
}
else {
xhr.send(options.file);
}
},
failure: function (f) { reject(f); }
});
});
}
/**
* A function called for progress on an upload
* @callback Context~slideUploadProgressCallback
* @param {ImageUpload~Slide} slide - The slide uploaded
* @param {Number} progress - The progress percentage
* @param {boolean} [hasFinished] - Whether the upload has finished
*/
/**
* A function called for result of an upload
* @callback Context~slideUploadResultCallback
* @param {ImageUpload~Slide} slide - The slide uploaded
*/
/**
* Uploads a file to the pma.core server
* @param {Object} options
* @param {string} options.serverUrl - The server url to use
* @param {string} options.targetPath - The virtual path to upload to
* @param {string} options.targetChildPath - The name of the directory containing auxiliary files for multi-file formats
* @param {ImageUpload~Slide} options.slide - The file object to upload
* @param {Context~slideUploadProgressCallback} options.progress - A callback called on progress
* @param {Context~slideUploadResultCallback} options.success - A callback called on success
* @param {Context~slideUploadResultCallback} options.error - A callback called on error
* @returns A promise to get the result of the upload
*/
uploadSlide(options) {
const that = this;
const files = options.slide.files;
const totalSize = files.reduce((sum, file) => sum + (file?.size || 0), 0);
const uploadedSize = new Array(files.length).fill(0);
function updateProgress(index, loaded) {
uploadedSize[index] = loaded;
const totalUploaded = uploadedSize.reduce((a, b) => a + b, 0);
const progress = Math.floor((totalUploaded / totalSize) * 100);
if (progress !== options.slide.uploadProgress && typeof options.progress === "function") {
options.progress(options.slide, progress);
}
}
function buildUploadUrls(serverUrl, sessionId, uploadId, files) {
return files.map((file) => {
return `${serverUrl}transfer/Upload/${uploadId}?sessionId=${sessionId}&path=${encodeURIComponent(file.name)}`;
});
}
return new Promise((resolve) => {
callApiMethodWithAuthentication.call(that, {
attemptCount: 0,
serverUrl: options.serverUrl,
method: ApiMethods.Upload,
apiPath: "transfer",
webapi: true,
httpMethod: "POST",
contentType: "application/json",
data: {
path: options.targetPath,
files: files.map((file, index) => ({
isMain: index === 0,
length: file.size,
path: index === 0 ? file.name : `${options.targetChildPath}/${file.name}`,
})),
},
success(sessionId, uploadContent) {
const isAzure = uploadContent.UploadType === 2;
const isAmazon = uploadContent.UploadType === 1;
const urls = (uploadContent.Urls?.length || uploadContent.MultipartFiles?.length) ? uploadContent.Urls : buildUploadUrls(options.serverUrl, sessionId, uploadContent.Id, files);
let completed = 0;
if (typeof options.progress === "function") {
options.progress(options.slide, 0);
}
files.forEach((file, index) => {
const url = uploadContent.MultipartFiles?.length ? null : urls[index];
const multipartFile = uploadContent.MultipartFiles?.[index];
function pollImageInfo(serverUrl, imagePath, successCallback, failureCallback, attempts = 0, maxAttempts = 5, interval = 1000) {
that.getImageInfo(
serverUrl,
imagePath,
() => {
if (typeof successCallback === "function") {
successCallback();
}
},
() => {
attempts++;
if (attempts < maxAttempts) {
setTimeout(() => pollImageInfo(serverUrl, imagePath, successCallback, failureCallback, attempts, maxAttempts, interval), attempts * interval);
} else if (typeof failureCallback === "function") {
failureCallback();
}
}
);
}
const onComplete = () => {
uploadedSize[index] = file.size;
completed++;
if (completed === files.length) {
options.progress(options.slide, 100, true);
fetch(`${options.serverUrl}transfer/Upload/${uploadContent.Id}?sessionId=${sessionId}`)
.then(() => {
const imagePath = (options.targetPath.endsWith("/") ? options.targetPath : options.targetPath + "/") + files[0].name;
pollImageInfo(
options.serverUrl,
imagePath,
() => {
if (typeof options.success === "function") {
options.success(options.slide);
}
resolve(true);
},
() => {
if (typeof options.error === "function") {
options.error(options.slide, "Slide upload completed, but unable to fetch image information.");
}
resolve(false);
}
);
});
}
};
const onError = (msg) => {
if (typeof options.error === "function") options.error(options.slide, msg);
resolve(false);
};
if (isAzure) {
uploadToAzureBlob(url, file, index, updateProgress, onComplete, onError);
} else if (isAmazon && uploadContent.MultipartFiles?.length) {
uploadToAmazonS3MultiPart(options.targetPath, multipartFile, file, index, updateProgress, onComplete, onError);
} else {
let method = "PUT";
if (!uploadContent.Urls || uploadContent.Urls.length === 0) {
method = "POST";
}
uploadStandard(method, url, file, index, updateProgress, onComplete, onError);
}
});
},
failure(err) {
if (typeof options.error === "function") {
options.error(options.slide, err?.Message || "Upload session initialization failed");
}
resolve(false);
},
});
});
// --- HELPER FUNCTIONS ---
/** Uploads a file using standard XMLHttpRequest
* @param {string} method - The HTTP method to use (POST or PUT)
* @param {string} url - The URL to upload the file to
* @param {File} file - The file to upload
* @param {number} index - The index of the file in the upload list
* @param {function} onProgress - Callback for upload progress
* @param {function} onComplete - Callback for upload completion
* @param {function} onError - Callback for upload error
* @returns {void}
* */
function uploadStandard(method, url, file, index, onProgress, onComplete, onError) {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append("file", file, file.name);
xhr.upload.onprogress = (e) => onProgress(index, e.loaded);
xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? onComplete() : onError(xhr.responseText));
xhr.onerror = () => onError("Upload failed");
xhr.open(method, url, true);
method === "POST" ? xhr.send(formData) : xhr.send(file);
}
/** Uploads a file to Amazon S3 using multipart upload
* @param {MultipartFile} multipartFile - The multipart file object containing upload details
* @param {File} file - The file to upload
* @param {number} index - The index of the file in the upload list
* @param {function} onProgress - Callback for upload progress
* @param {function} onComplete - Callback for upload completion
* @param {function} onError - Callback for upload error
* @returns {void}
*/
/** MultipartFile
* @typedef {Object} MultipartFile
* @property {string} FilePath - The path of the file in the multipart upload
* @property {string} UploadId - The ID of the multipart upload
* @property {Array<MultipartFilePart>} Parts - The list of uploaded part details
*/
/** MultipartFilePart
* @typedef {Object} MultipartFilePart
* @property {number} PartNumber - The part number of the uploaded part
* @property {number} Url - The URL to upload the part to
* @property {number} RangeStart - The start byte range of the uploaded part
* @property {number} RangeEnd - The end byte range of the uploaded part
*/
function uploadToAmazonS3MultiPart(targetPath, multipartFile, file, index, onProgress, onComplete, onError) {
const parts = multipartFile.Parts || [];
const uploadId = multipartFile.UploadId;
const eTags = [];
const progress = [];
function multipartComplete() {
callApiMethodWithAuthentication.call(that, {
attemptCount: 0,
serverUrl: options.serverUrl,
method: "Upload/CompleteMultipart",
apiPath: "transfer",
webapi: true,
httpMethod: "POST",
contentType: "application/json",
data: {
FilePath: options.targetPath + "/" + multipartFile.FilePath,
uploadId: uploadId,
parts: eTags.map((etag) => ({
PartNumber: etag.PartNumber,
ETag: etag.ETag
})),
},
success: function (sessionId, uploadContent) {
if (typeof onComplete === "function") {
onComplete();
}
},
failure: function (f) {
if (typeof onError === "function") {
onError("Multipart upload completion failed");
}
}
});
}
const uploadPart = (part) => {
const xhr = new XMLHttpRequest();
xhr.open("PUT", part.Url, true);
xhr.upload.onprogress = (e) => {
progress[part.PartNumber] = e.loaded;
onProgress(index, progress.reduce((sum, curr) => sum + curr, 0));
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
onProgress(index, progress.reduce((sum, curr) => sum + curr, 0));
} else {
onError(xhr.responseText);
}
};
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
// Get the ETag from the response header
const eTag = xhr.getResponseHeader("ETag");
// Store the ETag and part number
var etag = {
"PartNumber": part.PartNumber,
"ETag": eTag
};
eTags.push(etag);
// Call this when all parts have been uploaded
if (eTags.length === parts.length) {
multipartComplete();
}
}
};
xhr.onerror = () => onError("Part upload failed");
xhr.send(file.slice(part.RangeStart, part.RangeEnd + 1));
};
parts.forEach(uploadPart);
}
/** Uploads a file to Azure Blob Storage using block blobs
* @param {string} url - The URL to upload the block blob
* @param {File} file - The file to upload
* @param {number} index - The index of the file in the upload list
* @param {function} onProgress - Callback for upload progress
* @param {function} onComplete - Callback for upload completion
* @param {function} onError - Callback for upload error
* @returns {void}
*/
function uploadToAzureBlob(url, file, index, onProgress, onComplete, onError) {
const reader = new FileReader();
const blockIds = [];
const blockSize = 1024 * 1024 * 1024;
let pointer = 0;
function commitBlocks() {
const xml = `<?xml version="1.0" encoding="utf-8"?><BlockList>${blockIds.map((id) => `<Latest>${id}</Latest>`).join("")}</BlockList>`;
const xhr = new XMLHttpRequest();
xhr.open("PUT", `${url}&comp=blocklist`, true);
xhr.setRequestHeader("x-ms-blob-content-type", "application/octet-stream");
xhr.setRequestHeader("Content-Length", xml.length);
xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? onComplete() : onError(xhr.responseText));
xhr.onerror = () => onError("Failed to commit block list");
xhr.send(xml);
}
function sendNextBlock() {
if (pointer >= file.size) return commitBlocks();
const end = Math.min(pointer + blockSize, file.size);
const slice = file.slice(pointer, end);
const blockId = btoa(`block_${blockIds.length.toString().padStart(6, "0")}`);
blockIds.push(blockId);
reader.onloadend = () => {
const data = new Uint8Array(reader.result);
const xhr = new XMLHttpRequest();
xhr.open("PUT", `${url}&comp=block&blockid=${blockId}`, true);
xhr.setRequestHeader("x-ms-blob-type", "BlockBlob");
xhr.setRequestHeader("Content-Length", data.length);
xhr.upload.onprogress = (e) => onProgress(index, pointer + e.loaded);
xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? ((pointer = end), sendNextBlock()) : onError(xhr.responseText));
xhr.onerror = () => onError("Block upload failed");
xhr.send(data);
};
reader.readAsArrayBuffer(slice);
}
sendNextBlock();
}
}
}
// private methods
// finds the first authentication provider that handles the requested server
// and calls it's authenticate method
function authenticateWithProvider(serverUrl, success, failure) {
// search all authentication providers apart from PromptLogin
var i = 0;
for (i = 0; i < this.authenticationProviders.length; i++) {
if (this.authenticationProviders[i] instanceof PromptLogin) {
continue;
}
if (this.authenticationProviders[i].authenticate(serverUrl, success, failure)) {
return;
}
}
// if none of the previous providers were able to provide a session, search for a PromptLogin and attempt to authenticate with it
for (i = 0; i < this.authenticationProviders.length; i++) {
if (this.authenticationProviders[i] instanceof PromptLogin) {
if (this.authenticationProviders[i].authenticate(serverUrl, success, failure)) {
return;
}
break;
}
}
// no provider found, call failure
if (typeof failure === "function") {
failure({ Message: Resources.translate("Authentication failed at server {serverUrl}.", { serverUrl: serverUrl }) });
}
}
/**
* calls an API method that requires a session ID. This method will first attempt to acquire
* a session ID (possibly a cached one) and then call the requested method.
* If the method call fails because the call was unauthorized (so possible the session ID was not good),
* the method will be called again for a second time, this time forcing an authentication before the
* actual call.
* @param {object} options - The parameters to pass to the ajax request
* @param {number} options.attemptCount - Current attempt count
* @param {string} options.serverUrl - The URL of the server to send the request to
* @param {string} options.method - The API method to call
* @param {object} options.data - The data to send
* @param {string} options.httpMethod - The HTTP method to use
* @param {string} options.contentType - The content type of the request
* @param {function} options.success - Called upon success
* @param {function} options.failure - Called upon failure
* @param {string} [options.apiPath="api"] - The API path to append to the server URL
* @param {boolean} [options.webapi=false] - Whether the api call is a webapi call
* @fires PMA.UI.Components.Events.SessionIdLoginFailed
* @ignore
*/
function callApiMethodWithAuthentication(options) {
var _this = this;
if (!options.apiPath) {
options.apiPath = "api";
}
_this.getSession(options.serverUrl,
function (sessionId) {
// we have a session ID, try to call the actual method
options.data.sessionID = sessionId;
callApiMethod({
serverUrl: options.serverUrl,
method: options.method,
data: options.data,
contentType: options.contentType,
httpMethod: options.httpMethod,
apiPath: options.apiPath,
webapi: options.webapi,
success: function (http) {
// the call succeeded, parse data and call success
if (typeof options.success === "function") {
var response = parseJson(http.responseText);
options.success(options.data.sessionID, response);
}
},
failure: function (http) {
// failed, clean up the session ID that was possibly cached for this server
_sessionList.set(options.serverUrl, null);
// if it's the first attempt, try again, otherwise fail for good
if (http.status == 0 && options.attemptCount === 0) {
options.attemptCount = 1;
callApiMethodWithAuthentication.call(_this, options);
}
else {
if (typeof options.failure === "function") {
if (http.responseText && http.responseText.length !== 0) {
try {
var response = parseJson(http.responseText);
options.failure(response);
_this.fireEvent(Events.SessionIdLoginFailed, { serverUrl: options.serverUrl });
}
catch (ex) {
options.failure(http.responseText);
_this.fireEvent(Events.SessionIdLoginFailed, { serverUrl: options.serverUrl });
}
}
else {
options.failure({ Message: Resources.translate("Authentication failed") });
_this.fireEvent(Events.SessionIdLoginFailed, { serverUrl: options.serverUrl });
}
}
}
}
});
},
function (error) {
if (!error.Message && error.Reason) {
error.Message = error.Reason;
}
// session acquisition failed in the first place, so fail
if (typeof options.failure === "function") {
options.failure(error);
}
_this.fireEvent(Events.SessionIdLoginFailed, { serverUrl: options.serverUrl });
});
}
// legacy alias because of typo
Context.prototype.GetImagesInfo = Context.prototype.getImagesInfo;