/* global Components, Services */
/**
 * @copyright Copyright (c) 2017, Olivier Paroz (github@oparoz.com)
 * @copyright Copyright (c) 2017, Philipp Kewisch
 * @copyright Copyright (c) 2017, Mark James
 * @copyright Copyright (c) 2017, Guillaume Viguier-Just (@guillaumev)
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/**
 * This file implements the nsIMsgCloudFileProvider interface.
 *
 * This component handles the Nextcloud implementation of the nsIMsgCloudFileProvider interface.
 */

const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/oauth.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/gloda/log4moz.js");
Cu.import("resource:///modules/cloudFileAccounts.js");

const kRestBase = "/ocs/v1.php";
const kAuthPath = kRestBase + "/cloud/user";
const kShareApp = kRestBase + "/apps/files_sharing/api/v1/shares";
const kWebDavPath = "/remote.php/webdav";

function wwwFormUrlEncode (aStr) {
	return encodeURIComponent(aStr)
		.replace(/!/g, '%21')
		.replace(/'/g, '%27')
		.replace(/\(/g, '%28')
		.replace(/\)/g, '%29')
		.replace(/\*/g, '%2A')
		.replace(/\@/g, '%40');
}

function validURL(value) {
  return /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(value);
}

/**
 * Our Nextcloud Provider
 */
function Nextcloud () {
	//this.log = Log4Moz.getConfiguredLogger("Nextcloud", Log4Moz.Level.Info, Log4Moz.Level.Debug,
	//  Log4Moz.Level.Debug);
	this.log = Log4Moz.getConfiguredLogger("Nextcloud");
}

Nextcloud.prototype = {
	/* nsISupports */
	QueryInterface: XPCOMUtils.generateQI([Ci.nsIMsgCloudFileProvider]),

	classID: Components.ID("{ad8c3b77-7dc8-41d1-8985-5be88b254ff3}"),

	get type () {
		return "Nextcloud";
	},
	get displayName () {
		return this._displayName;
	},
	get serviceURL () {
		return this._serverUrl;
	},
	get iconClass () {
		return "chrome://messenger/skin/icons/nextcloud.png";
	},
	get accountKey () {
		return this._accountKey;
	},
	get lastError () {
		return this._lastErrorText;
	},
	get settingsURL () {
		return "chrome://messenger/content/cloudfile/NextCloud/settings.xhtml";
	},
	get managementURL () {
		return "chrome://messenger/content/cloudfile/NextCloud/management.xhtml";
	},

	/**
	 * If the provider doesn't have an API for creating an account, perhaps
	 * there's a url we can load in a content tab that will allow the user
	 * to create an account.
	 */
	get createNewAccountUrl () {
		return "";
	},

	_displayName: "Nextcloud",
	_accountKey: false,
	_serverUrl: "",
	_serverPort: 443,
	_fullUrl: "",
	_storageFolder: "",
	_userName: "",
	_password: "",
	_protectUploads: "",
	_prefBranch: null,
	_loggedIn: false,
	_authToken: "",
	_userInfo: null,
	_file: null,
	_requestDate: null,
	_successCallback: null,
	_connection: null,
	_request: null,
	_uploadingFile: null,
	_uploader: null,
	_lastErrorStatus: 0,
	_lastErrorText: "",
	_maxFileSize: -1,
	_totalStorage: -1,
	_fileSpaceUsed: -1,
	_uploads: [],
	_urlsForFiles: {},
	_uploadInfo: {}, // upload info keyed on aFiles.

	/**
	 * Initialize this instance of Nextcloud, setting the accountKey.
	 *
	 * @param aAccountKey The account key that this instance of the nsIMsgCloudFileProvider should
	 *     be associated with.
	 */
	init: function nsNc_init (aAccountKey) {
		this._accountKey = aAccountKey;
		this._prefBranch = Services.prefs.getBranch("mail.cloud_files.accounts." +
			aAccountKey + ".");

		if (this._prefBranch.getCharPref("displayName") != "") {
			this._displayName = this._prefBranch.getCharPref("displayName");
		}

		this._serverUrl = this._prefBranch.getCharPref("server");
		this._serverPort = this._prefBranch.getIntPref("port");
		this._userName = this._prefBranch.getCharPref("username");
		let fullUrl = this._serverUrl + ":" + this._serverPort;
		const thirdSlash = this._serverUrl.indexOf('/', 9);
		if (thirdSlash !== -1) {
			fullUrl = this._serverUrl.slice(0, thirdSlash) + ":" + this._serverPort + this._serverUrl.slice(thirdSlash);
		}
		this._fullUrl = fullUrl;

		if (this._prefBranch.prefHasUserValue("storageFolder")) {
			this._storageFolder = this._prefBranch.getCharPref("storageFolder");
		} else {
			this._storageFolder = "/Mail-attachments";
		}

		if (this._prefBranch.prefHasUserValue("protectUploads")) {
			this._protectUploads = this._prefBranch.getCharPref("protectUploads");
		}
	},

	/**
	 * Attempts to upload a file to Nextcloud.
	 *
	 * @param aFile the nsILocalFile to be uploaded
	 * @param aCallback an nsIRequestObserver for listening for the starting
	 *                  and ending states of the upload.
	 */
	uploadFile: function nsNc_uploadFile (aFile, aCallback) {
		if (Services.io.offline) {
			throw Ci.nsIMsgCloudFileProvider.offlineErr;
		}

		this.log.info("uploading " + aFile.leafName);

		// Some ugliness here - we stash requestObserver here, because we might
		// use it again in _getUserInfo.
		this.requestObserver = aCallback;

		// if we're uploading a file, queue this request.
		if (this._uploadingFile && this._uploadingFile != aFile) {
			let uploader = new NextcloudFileUploader(this, aFile, this._uploaderCallback
				.bind(this), aCallback);
			this._uploads.push(uploader);
			return;
		}
		this._file = aFile;
		this._uploadingFile = aFile;

		let successCallback = this._finishUpload.bind(this, aFile, aCallback);
		if (!this._loggedIn) {
			return this._logonAndGetUserInfo(successCallback, null, true);
		}

		this.log.info("getting user info");

		if (!this._userInfo) {
			return this._getUserInfo(successCallback);
		}
		successCallback();
	},

	/**
	 * Attempts to cancel a file upload.
	 *
	 * @param aFile the nsILocalFile to cancel the upload for.
	 */
	cancelFileUpload: function nsNc_cancelFileUpload (aFile) {
		if (this._uploadingFile.equals(aFile)) {
			this._uploader.cancel();
		} else {
			for (let i = 0; i < this._uploads.length; i++)
				if (this._uploads[i].file.equals(aFile)) {
					this._uploads[i].requestObserver.onStopRequest(
						null, null, Ci.nsIMsgCloudFileProvider.uploadCanceled
					);
					this._uploads.splice(i, 1);
					return;
				}
		}
	},

	/**
	 * For some nsILocalFile, return the associated sharing URL.
	 *
	 * @param aFile the nsILocalFile to retrieve the URL for
	 */
	urlForFile: function nsNc_urlForFile (aFile) {
		return this._urlsForFiles[aFile.path];
	},

	/**
	 * Updates the profile information for the account associated with the
	 * account key.
	 *
	 * @param aWithUI a boolean for whether or not we should display authorization
	 *                UI if we don't have a valid token anymore, or just fail out.
	 * @param aCallback an nsIRequestObserver for observing the starting and
	 *                  ending states of the request.
	 */
	refreshUserInfo: function nsNc_refreshUserInfo (aWithUI, aCallback) {
		if (Services.io.offline) {
			throw Ci.nsIMsgCloudFileProvider.offlineErr;
		}
		this.requestObserver = aCallback;
		aCallback.onStartRequest(null, null);
		if (!this._loggedIn) {
			return this._logonAndGetUserInfo(null, null, aWithUI);
		}
		if (!this._userInfo) {
			return this._getUserInfo();
		}
		return this._userInfo;
	},

	/**
	 * Allows for the creation of a new user on the Nextcloud instance.
	 *
	 * This implementation does not implement the createNewAccount
	 * function defined in nsIMsgCloudFileProvider.idl.
	 */
	createNewAccount: function nsNc_createNewAccount (aEmailAddress, aPassword, aFirstName, aLastName) {
		return Cr.NS_ERROR_NOT_IMPLEMENTED;
	},

	/**
	 * If the user already has an account on a Nextcloud instance, we can get the user to just login
	 *
	 * This function does not appear to be called from the BigFiles UI, and
	 * might be excisable.
	 */
	createExistingAccount: function nsNc_createExistingAccount (aRequestObserver) {
		// XXX: replace this with a better function
		let successCb = function () {

			let callbackCreateFolder = function (created) {
				if (created) {
					aRequestObserver.onStopRequest(null, this, Cr.NS_OK);
				}
				else {
					aRequestObserver.onStopRequest(null, this, Ci.nsIMsgCloudFileProvider.authErr);
				}
			}.bind(this);

			let folderExists = function (exists) {
				if (exists) {
					aRequestObserver.onStopRequest(null, this, Cr.NS_OK);
				}
				else {
					this._createFolder(callbackCreateFolder);
				}
			}.bind(this);
			this._checkFolderExists(folderExists);
		}.bind(this);

		let failureCb = function () {
			aRequestObserver.onStopRequest(null, this, Ci.nsIMsgCloudFileProvider.authErr);
		}.bind(this);

		this.logon(successCb, failureCb, true);
	},

	/**
	 * For a particular error, return a URL if Nextcloud has a page for handling
	 * that particular error.
	 *
	 * @param aError the error to get the URL for
	 */
	providerUrlForError: function nsNc_providerUrlForError (aError) {
		return "";
	},

	/**
	 * If we don't know the limit, this will return -1.
	 */
	get fileUploadSizeLimit () {
		return this._maxFileSize;
	},
	get remainingFileSpace () {
		return this._totalStorage > 0 ? this._totalStorage - this._fileSpaceUsed : -1;
	},
	get fileSpaceUsed () {
		return this._fileSpaceUsed;
	},

	/**
	 * Attempt to delete an upload file if we've uploaded it.
	 *
	 * @param aFile the file that was originall uploaded
	 * @param aCallback an nsIRequestObserver for monitoring the starting and
	 *                  ending states of the deletion request.
	 */
	deleteFile: function nsNc_deleteFile (aFile, aCallback) {
		return Cr.NS_ERROR_NOT_IMPLEMENTED;
	},

	/**
	 * Returns the saved password for this account if one exists, or prompts
	 * the user for a password. Returns the empty string on failure.
	 *
	 * @param aUsername the username associated with the account / password.
	 * @param aNoPrompt a boolean for whether or not we should suppress
	 *                  the password prompt if no password exists.  If so,
	 *                  returns the empty string if no password exists.
	 */
	getPassword: function (aUsername, aNoPrompt) {
		this.log.info("Getting password for user: " + aUsername);

		if (aNoPrompt) {
			this.log.info("Suppressing password prompt");
		}

		let passwordURI = this._fullUrl;
		let logins = Services.logins.findLogins({}, passwordURI, null, passwordURI);
		for (let loginInfo of logins) {
			if (loginInfo.username == aUsername) {
				return loginInfo.password;
			}
		}
		if (aNoPrompt) {
			return "";
		}

		// OK, let's prompt for it.
		let win = Services.wm.getMostRecentWindow(null);

		let authPrompter = Services.ww.getNewAuthPrompter(win);
		let password = {value: ""};
		// Use the service name in the prompt text
		let userPos = this._fullUrl.indexOf("//") + 2;
		let userNamePart = encodeURIComponent(this._userName) + '@';
		let serverUrl =
			this._fullUrl.substr(0, userPos) + userNamePart + this._fullUrl.substr(userPos);
		let messengerBundle = Services.strings.createBundle(
			"chrome://messenger/locale/messenger.properties");
		let promptString = messengerBundle.formatStringFromName("passwordPrompt",
			[
				this._userName,
				this.displayName
			],
			2);

		if (validURL(serverUrl) && authPrompter.promptPassword(
				this.displayName,
				promptString,
				serverUrl,
				authPrompter.SAVE_PASSWORD_PERMANENTLY,
				password)) {
			return password.value;
		}

		return "";
	},

	/**
	 * This function is used by our testing framework to override the default
	 * URL's that Nextcloud connects to.
	 */
	overrideUrls: function nsNc_overrideUrls (aNumUrls, aUrls) {
		this._serverUrl = aUrls[0];
	},

	/**
	 * logon to the Nextcloud account.
	 *
	 * @param successCallback - called if logon is successful
	 * @param failureCallback - called back on error.
	 * @param aWithUI if false, logon fails if it would have needed to put up UI.
	 *                This is used for things like displaying account settings,
	 *                where we don't want to pop up the oauth ui.
	 */
	logon: function nsNc_logon (successCallback, failureCallback, aWithUI) {
		this.log.info("Logging in, aWithUI = " + aWithUI);

		if (this._password == undefined || !this._password) {
			this._password = this.getPassword(this._userName, !aWithUI);
		}

		if (this._password == "") {
			this.log.error('Could not retrieve password');
			this._loggedIn = false;
			failureCallback();
		}

		this.log.info("Sending login information...");

		let args = "?format=json";
		let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
			.createInstance(Ci.nsIXMLHttpRequest);

		req.open("GET", this._fullUrl + kAuthPath + args, true);
		req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
		req.setRequestHeader("OCS-APIREQUEST", "true");
		req.setRequestHeader("Authorization",
			"Basic " + btoa(this._userName + ':' + this._password));

		req.onerror = function () {
			this.log.info("logon failure");

			failureCallback();
		}.bind(this);

		req.onload = function () {
			if (req.status >= 200 && req.status < 400) {
				try {
					this.log.info("auth token response = " + req.responseText);

					let docResponse = JSON.parse(req.responseText);
					//this.log.info("login response parsed = " + docResponse);
					let statuscode = docResponse.ocs.meta.statuscode;

					this.log.info("statuscode = " + statuscode);

					if (statuscode == 100) {
						this._loggedIn = true;
						successCallback();
					}
					else {
						this._loggedIn = false;
						this._lastErrorText = docResponse.ocs.meta.message;
						this._lastErrorStatus = docResponse.ocs.meta.statuscode;
						failureCallback();
					}
				} catch (e) {
					this.log.error(e);
					this._loggedIn = false;
					failureCallback();
				}
			}
			else {
				failureCallback();
			}
		}.bind(this);

		req.send();
		this.log.info("Login information sent!");
	},

	/**
	 * The callback passed to an NextcloudFileUploader, which is fired when
	 * NextcloudFileUploader exits.
	 *
	 * @param aRequestObserver the request observer originally passed to
	 *                         uploadFile for the file associated with the
	 *                         NextcloudFileUploader
	 * @param aStatus the result of the upload
	 * @private
	 */
	_uploaderCallback: function nsNc__uploaderCallback (aRequestObserver, aStatus) {
		aRequestObserver.onStopRequest(null, null, aStatus);
		this._uploadingFile = null;
		this._uploads.shift();
		if (this._uploads.length > 0) {
			let nextUpload = this._uploads[0];
			this.log.info("chaining upload, file = " + nextUpload.file.leafName);
			this._uploadingFile = nextUpload.file;
			this._uploader = nextUpload;
			try {
				this.uploadFile(nextUpload.file, nextUpload.callback);
			}
			catch (ex) {
				nextUpload.callback(nextUpload.requestObserver, Cr.NS_ERROR_FAILURE);
			}
		}
		else
			this._uploader = null;
	},

	/**
	 * Ensures that we can actually upload the file (we haven't exceeded file size or quota
	 * limitations), and then attempts to kick-off the upload.
	 *
	 * @param aFile the nsILocalFile to upload
	 * @param aCallback an nsIRequestObserver for monitoring the starting and
	 *                  ending states of the upload.
	 * @private
	 */
	_finishUpload: function nsNc_finishUpload (aFile, aCallback) {
		let exceedsQuota = Ci.nsIMsgCloudFileProvider.uploadWouldExceedQuota;
		if ((this._totalStorage > 0) && (aFile.fileSize > this.remainingFileSpace)) {
			return aCallback.onStopRequest(null, null, exceedsQuota);
		}

		delete this._userInfo; // forces us to update userInfo on every upload.

		if (!this._uploader) {
			this._uploader = new NextcloudFileUploader(this, aFile, this._uploaderCallback
				.bind(this), aCallback);
			this._uploads.unshift(this._uploader);
		}

		this._uploadingFile = aFile;
		this._uploader.uploadFile();
	},

	/**
	 * A private function used to retrieve the profile information for the
	 * user account associated with the accountKey.
	 *
	 * @param successCallback the function called if information retrieval
	 *                        is successful
	 * @param failureCallback the function called if information retrieval fails
	 * @private
	 */
	_getUserInfo: function nsNc_getUserInfo (successCallback, failureCallback) {
		if (!successCallback) {
			successCallback = function () {
				this.requestObserver
					.onStopRequest(null, null,
						this._loggedIn ? Cr.NS_OK : Ci.nsIMsgCloudFileProvider.authErr);
			}.bind(this);
		}

		if (!failureCallback) {
			failureCallback = function () {
				this.requestObserver
					.onStopRequest(null, null, Ci.nsIMsgCloudFileProvider.authErr);
			}.bind(this);
		}

		let body = '<propfind xmlns="DAV:">' +
			'<prop>' +
			'<quota-available-bytes/>' +
			'<quota-used-bytes/>' +
			'</prop>' +
			'</propfind>';

		let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(
			Ci.nsIXMLHttpRequest);

		req.open("PROPFIND", this._fullUrl + kWebDavPath, true,
			this._userName, this._password);
		req.onerror = function () {
			this.log.info("logon failure");
			failureCallback();
		}.bind(this);

		req.onload = function () {
			if (req.status >= 200 && req.status < 400) {
				this._fileSpaceUsed = this._getQuota(req.responseXML, "quota-used-bytes");
				if (this._fileSpaceUsed < 0) {
					this._fileSpaceUsed = -1;
				}

				let spaceAvailable = this._getQuota(req.responseXML, "quota-available-bytes");

				if (spaceAvailable && spaceAvailable > -1) { // positive numbers
					this._totalStorage = spaceAvailable + this._fileSpaceUsed;
				} else if (!spaceAvailable && spaceAvailable !== 0) { // 0 and unequal 0
					this._totalStorage = -1;
				} else if (!spaceAvailable || spaceAvailable < 0) { // 0 or negative
					this._totalStorage = 0;
				}
				successCallback();
			} else {
				failureCallback();
			}
		}.bind(this);

		req.send(body);
	},

	/**
	 * Retrieves quota information from a WebDAV response
	 *
	 * @param req
	 * @param davVariable
	 * @returns {NodeList|number}
	 * @private
	 */
	_getQuota: function nsNc_getUserQuota (req, davVariable) {
		let quota = req.documentElement.getElementsByTagNameNS("DAV:", davVariable);
		return quota && quota.length && Number(quota[0].textContent) || -1;
	},

	/**
	 * A private function which makes sure that the folder entered in the
	 * settings screen exists on the server.
	 *
	 * @param callback callback function called with true or false as an argument
	 * @private
	 */
	_checkFolderExists: function nsNc_checkFolderExists (callback) {
		if (this._storageFolder !== '/') {
			let body = '<propfind xmlns="DAV:">' +
				'<prop>' +
				'<resourcetype />' +
				'</prop>' +
				'</propfind>';

			let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
				.createInstance(Ci.nsIXMLHttpRequest);

			req.open("PROPFIND", this._fullUrl + kWebDavPath +
				("/" + this._storageFolder + "/").replace(/\/+/g, '/'), true, this._userName,
				this._password);
			req.onerror = function () {
				this.log.info("Failed to check if folder exists");
				callback(false);
			}.bind(this);

			req.onload = function () {
				if (req.status === 207) {
					return callback(true);
				}
				else {
					return callback(false);
				}
			}.bind(this);

			req.send(body);
		}
		else {
			return callback(true);
		}
	},

	/**
	 * A private function which creates a folder entered
	 * in the settings screen on the server.
	 *
	 * @param callback callback function called with true or false as an argument
	 * @private
	 */
	_createFolder: function createFolder(callback) {
		if (this._storageFolder !== '/') {
			let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
				.createInstance(Ci.nsIXMLHttpRequest);

			req.open("MKCOL", this._fullUrl + kWebDavPath +
				("/" + this._storageFolder + "/").replace(/\/+/g, '/'), true, this._userName,
				this._password);

			req.onload = function () {
				if (req.status === 201) {
					return callback(true);
				}
				else {
					return callback(false);
				}
			}.bind(this);
			req.send();
		}
		else {
			return callback(false);
		}

	},

	/**
	 * A private function that first ensures that the user is logged in, and then
	 * retrieves the user's profile information.
	 *
	 * @param aSuccessCallback the function called on successful information
	 *                         retrieval
	 * @param aFailureCallback the function called on failed information retrieval
	 * @param aWithUI a boolean for whether or not we should display authorization
	 *                UI if we don't have a valid token anymore, or just fail out.
	 * @private
	 */
	_logonAndGetUserInfo: function nsNc_logonAndGetUserInfo (aSuccessCallback,
															 aFailureCallback,
															 aWithUI) {
		if (!aFailureCallback) {
			aFailureCallback = function () {
				this.requestObserver.onStopRequest(null, null, Ci.nsIMsgCloudFileProvider.authErr);
			}.bind(this);
		}

		return this.logon(function () {
			this._getUserInfo(aSuccessCallback, aFailureCallback);
		}.bind(this), aFailureCallback, aWithUI);
	},
};

/**
 * Uploads a file to the Nextcloud server
 *
 * @type {{nextcloud: null, file: null, callback: null, request: null, _fileUploadTS: {},
 *     uploadFile: NextcloudFileUploader.nsNCFU_uploadFile, cancel:
 *     NextcloudFileUploader.nsNCFU_cancel, _getShareUrl:
 *     NextcloudFileUploader.nsNCFU__getShareUrl}}
 */
function NextcloudFileUploader (aNextcloud, aFile, aCallback, aRequestObserver) {
	this.nextcloud = aNextcloud;
	this.log = this.nextcloud.log;
	this.log.info("new NextcloudFileUploader file = " + aFile.leafName);
	this.file = aFile;
	this.callback = aCallback;
	this.requestObserver = aRequestObserver;
}

NextcloudFileUploader.prototype = {
	nextcloud: null,
	file: null,
	callback: null,
	request: null,
	_fileUploadTS: {}, // timestamps to prepend, avoiding filename conflict

	/**
	 * Kicks off the upload request for the file associated with this Uploader.
	 */
	uploadFile: function nsNCFU_uploadFile () {
		this.requestObserver.onStartRequest(null, null);
		this._fileUploadTS[this.file.path] = new Date().getTime();

		this.log.info(
			"Ready to upload file " + wwwFormUrlEncode(this.file.leafName) +
			" to folder " + this.nextcloud._storageFolder
		);

		let folder = ("/" + this.nextcloud._storageFolder + "/").replace(/\/+/g, '/');

		let url = this.nextcloud._fullUrl +
			kWebDavPath +
			folder +
			this._fileUploadTS[this.file.path] +
			"_" +
			this.file.leafName;

		let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
			Ci.nsIFileInputStream);
		fstream.init(this.file, -1, 0, 0);

		let bufStream = Cc["@mozilla.org/network/buffered-input-stream;1"].createInstance(
			Ci.nsIBufferedInputStream);
		bufStream.init(fstream, this.file.fileSize);
		bufStream = bufStream.QueryInterface(Ci.nsIInputStream);
		let contentLength = fstream.available();

		let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(
			Ci.nsIXMLHttpRequest);

		req.open("PUT", url, true, this._userName, this._password);

		req.onerror = function () {
			this.log.info("Could not upload file");
			if (this.callback) {
				this.callback(this.requestObserver,
					Ci.nsIMsgCloudFileProvider.uploadErr);
			}
		}.bind(this);

		req.onload = function () {
			if (req.status >= 200 && req.status < 400) {
				this._getShareUrl(this.file, this.callback);
			}
			else {
				if (this.callback)
					this.callback(this.requestObserver,
						Ci.nsIMsgCloudFileProvider.uploadErr);
			}
		}.bind(this);

		req.setRequestHeader("Content-Length", contentLength);
		req.send(bufStream);
	},

	/**
	 * Cancels the upload request for the file associated with this Uploader.
	 */
	cancel: function nsNCFU_cancel () {
		this.callback(this.requestObserver, Ci.nsIMsgCloudFileProvider.uploadCanceled);
		if (this.request) {
			let req = this.request;
			if (req.channel) {
				this.log.info("Cancelling channel upload");
				delete this.callback;
				req.channel.cancel(Cr.NS_BINDING_ABORTED);
			}
			this.request = null;
		}
	},

	/**
	 * Private function which attempts to retrieve the sharing URL for the file which was uploaded
	 * via this Uploader.
	 *
	 * @param aFile file which was just uploaded
	 * @param aCallback an nsIRequestObserver which monitors the start and end states of the URL
	 *     retrieval request.
	 * @private
	 */
	_getShareUrl: function nsNCFU_getShareUrl (aFile, aCallback) {
		this.file = aFile;
		let shareType = 3;
		let args = "?format=json";
		let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
			.createInstance(Ci.nsIXMLHttpRequest);
		let path = wwwFormUrlEncode(
			("/" + this.nextcloud._storageFolder + "/").replace(/\/+/g, '/') +
			this._fileUploadTS[this.file.path] + "_" + this.file.leafName);

		let formData = "shareType=" + shareType + "&path=" + path;
		// Request a password for the link if it has been defined during setup time
		if (this.nextcloud._protectUploads.length) {
			formData += "&password=" + wwwFormUrlEncode(this.nextcloud._protectUploads);
		}

		req.open("POST",
			this.nextcloud._fullUrl + kShareApp + args,
			true,
			this.nextcloud._userName,
			this.nextcloud._password
		);
		req.withCredentials = true;
		req.setRequestHeader('Content-Type', "application/x-www-form-urlencoded");
		req.setRequestHeader("Content-Length", String(formData.length));
		req.setRequestHeader("OCS-APIREQUEST", "true");
		req.setRequestHeader("Authorization",
			"Basic " + btoa(this.nextcloud._userName + ':' + this.nextcloud._password));

		req.onload = function () {

			this.log.debug("Raw response: " + req.responseText);

			if (req.status >= 200 && req.status < 400) {
				try {
					let response = JSON.parse(req.responseText);
					if (typeof response.ocs.data.url !== "undefined") {
						this.nextcloud._urlsForFiles[this.file.path] = response.ocs.data.url;
						if (!this.nextcloud._protectUploads.length) {
							let arg_separator = this.nextcloud._urlsForFiles[this.file.path].lastIndexOf(
								"?") > 0 ? "&" : "/";
							this.nextcloud._urlsForFiles[this.file.path] +=
								arg_separator + "download";
						}
						aCallback(this.requestObserver, Cr.NS_OK);
					} else {
						aCallback(this.requestObserver, Ci.nsIMsgCloudFileProvider.uploadErr);
					}
				} catch (e) {
					this.log.error(e);
					aCallback(this.requestObserver, Ci.nsIMsgCloudFileProvider.uploadErr);
				}
			} else {
				this.log.info("Could not retrieve share URL");
				aCallback(this.requestObserver, Cr.NS_ERROR_FAILURE);
			}
		}.bind(this);

		req.onerror = function (e) {
			this.log.debug("Other error: " + e);
			aCallback(this.requestObserver, Cr.NS_ERROR_FAILURE);
		}.bind(this);
		this.log.debug("Raw formData: " + formData);
		req.send(formData);
	}
};

const NSGetFactory = XPCOMUtils.generateNSGetFactory([Nextcloud]);
