From f3da60a54ef9bc5e8ab4aa23dd675f03e81367c5 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 22 Feb 2016 15:40:36 +0100 Subject: [PATCH 1/2] MOBILE-1408 filepool: Keep file names in fileId --- upgrade.txt | 1 + www/addons/mod_imscp/services/imscp.js | 6 +- www/addons/mod_resource/services/resource.js | 6 +- www/addons/mod_scorm/services/scorm.js | 4 +- www/core/lib/filepool.js | 371 +++++++++++++++---- www/core/lib/fs.js | 39 +- www/core/lib/text.js | 57 +++ 7 files changed, 401 insertions(+), 83 deletions(-) diff --git a/upgrade.txt b/upgrade.txt index fef158a0dfe..ef6010c05b9 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -4,6 +4,7 @@ information provided here is intended especially for developers. === 2.9 === * Most of the functions from $mmaModScormOnline, $mmaModScormOffline, $mmaModScorm, $mmaModScormSync and $mmaModScormHelper now have a new param siteId to determine the site to affect. If you use any of these services please make sure you still pass the right parameters. + * Now filepool tries to use the files names instead of just using a hash. If you used the functions "getFilePathByUrl" or "getDirectoryUrlByUrl" to store package files, you should now use "getPackageDirPathByUrl" and "getPackageDirUrlByUrl" instead. === 2.8 === diff --git a/www/addons/mod_imscp/services/imscp.js b/www/addons/mod_imscp/services/imscp.js index 4c564d82ecb..f0f132a7d29 100644 --- a/www/addons/mod_imscp/services/imscp.js +++ b/www/addons/mod_imscp/services/imscp.js @@ -144,7 +144,7 @@ angular.module('mm.addons.mod_imscp') revision = $mmFilepool.getRevisionFromFileList(module.contents), timemod = $mmFilepool.getTimemodifiedFromFileList(module.contents); - return $mmFilepool.getFilePathByUrl($mmSite.getId(), module.url).then(function(dirPath) { + return $mmFilepool.getPackageDirPathByUrl($mmSite.getId(), module.url).then(function(dirPath) { return $mmFilepool.downloadPackage($mmSite.getId(), files, mmaModImscpComponent, module.id, revision, timemod, dirPath); }); }; @@ -267,7 +267,7 @@ angular.module('mm.addons.mod_imscp') } mainFilePath = toc[0].href; - return $mmFilepool.getDirectoryUrlByUrl($mmSite.getId(), module.url).then(function(dirPath) { + return $mmFilepool.getPackageDirUrlByUrl($mmSite.getId(), module.url).then(function(dirPath) { currentDirPath = dirPath; // This URL is going to be injected in an iframe, we need trustAsResourceUrl to make it work in a browser. return $sce.trustAsResourceUrl($mmFS.concatenatePaths(dirPath, mainFilePath)); @@ -390,7 +390,7 @@ angular.module('mm.addons.mod_imscp') revision = $mmFilepool.getRevisionFromFileList(module.contents), timemod = $mmFilepool.getTimemodifiedFromFileList(module.contents); - return $mmFilepool.getFilePathByUrl($mmSite.getId(), module.url).then(function(dirPath) { + return $mmFilepool.getPackageDirPathByUrl($mmSite.getId(), module.url).then(function(dirPath) { return $mmFilepool.prefetchPackage($mmSite.getId(), files, mmaModImscpComponent, module.id, revision, timemod, dirPath); }); }; diff --git a/www/addons/mod_resource/services/resource.js b/www/addons/mod_resource/services/resource.js index f1e27bc041d..18729d22f40 100644 --- a/www/addons/mod_resource/services/resource.js +++ b/www/addons/mod_resource/services/resource.js @@ -45,7 +45,7 @@ angular.module('mm.addons.mod_resource') if (self.isDisplayedInIframe(module)) { // Get path of the module folder in filepool. - promise = $mmFilepool.getFilePathByUrl(siteid, module.url); + promise = $mmFilepool.getPackageDirPathByUrl(siteid, module.url); } else { promise = $q.when(); } @@ -153,7 +153,7 @@ angular.module('mm.addons.mod_resource') mainFilePath = mainFile.filepath.substr(1) + mainFilePath; } - return $mmFilepool.getDirectoryUrlByUrl($mmSite.getId(), module.url).then(function(dirPath) { + return $mmFilepool.getPackageDirUrlByUrl($mmSite.getId(), module.url).then(function(dirPath) { // This URL is going to be injected in an iframe, we need trustAsResourceUrl to make it work in a browser. return $sce.trustAsResourceUrl($mmFS.concatenatePaths(dirPath, mainFilePath)); }, function() { @@ -396,7 +396,7 @@ angular.module('mm.addons.mod_resource') if (self.isDisplayedInIframe(module)) { // Get path of the module folder in filepool. - promise = $mmFilepool.getFilePathByUrl(siteid, module.url); + promise = $mmFilepool.getPackageDirPathByUrl(siteid, module.url); } else { promise = $q.when(); } diff --git a/www/addons/mod_scorm/services/scorm.js b/www/addons/mod_scorm/services/scorm.js index 943b445407e..bdd867465ac 100644 --- a/www/addons/mod_scorm/services/scorm.js +++ b/www/addons/mod_scorm/services/scorm.js @@ -836,7 +836,7 @@ angular.module('mm.addons.mod_scorm') siteId = siteId || $mmSite.getId(); - return $mmFilepool.getDirectoryUrlByUrl(siteId, scorm.moduleurl).then(function(dirPath) { + return $mmFilepool.getPackageDirUrlByUrl(siteId, scorm.moduleurl).then(function(dirPath) { // This URL is going to be injected in an iframe, we need trustAsResourceUrl to make it work in a browser. return $sce.trustAsResourceUrl($mmFS.concatenatePaths(dirPath, sco.launch)); }); @@ -854,7 +854,7 @@ angular.module('mm.addons.mod_scorm') */ self.getScormFolder = function(moduleUrl, siteId) { siteId = siteId || $mmSite.getId(); - return $mmFilepool.getFilePathByUrl(siteId, moduleUrl); + return $mmFilepool.getPackageDirPathByUrl(siteId, moduleUrl); }; /** diff --git a/www/core/lib/filepool.js b/www/core/lib/filepool.js index 663cf132723..e7b294d8fb6 100644 --- a/www/core/lib/filepool.js +++ b/www/core/lib/filepool.js @@ -168,7 +168,7 @@ angular.module('mm.core') .factory('$mmFilepool', function($q, $log, $timeout, $mmApp, $mmFS, $mmWS, $mmSitesManager, $mmEvents, md5, mmFilepoolStore, mmFilepoolLinksStore, mmFilepoolQueueStore, mmFilepoolFolder, mmFilepoolQueueProcessInterval, mmCoreEventQueueEmpty, mmCoreDownloaded, mmCoreDownloading, mmCoreNotDownloaded, mmCoreOutdated, mmCoreNotDownloadable, mmFilepoolPackagesStore, - mmCoreEventPackageStatusChanged) { + mmCoreEventPackageStatusChanged, $mmText, $mmUtil) { $log = $log.getInstance('$mmFilepool'); @@ -719,11 +719,16 @@ angular.module('mm.core') promise; if ($mmFS.isAvailable()) { - return self._fixPluginfileURL(siteId, fileUrl).then(function(fileUrl) { + return self._fixPluginfileURL(siteId, fileUrl).then(function(fixedUrl) { + fileUrl = fixedUrl; timemodified = timemodified || 0; revision = self.getRevisionFromUrl(fileUrl); fileId = self._getFileIdByUrl(fileUrl); + // Restore old file if needed. + return self._restoreOldFileIfNeeded(siteId, fileId, fileUrl, filePath); + }).then(function() { + return self._hasFileInPool(siteId, fileId).then(function(fileObject) { if (typeof fileObject === 'undefined') { @@ -874,6 +879,23 @@ angular.module('mm.core') }); }; + /** + * Get the links of a file. + * + * @module mm.core + * @ngdoc method + * @name $mmFilepool#_getFileLinks + * @param {String} siteId The site ID. + * @param {String} fileId The file ID. + * @return {Promise} Promise resolved with the links. + * @protected + */ + self._getFileLinks = function(siteId, fileId) { + return getSiteDb(siteId).then(function(db) { + return db.query(mmFilepoolLinksStore, ['fileId', '=', fileId]); + }); + }; + /** * Get the ID of a file download. Used to keep track of filePromises. * @@ -1144,18 +1166,63 @@ angular.module('mm.core') /** * Creates a unique ID based on a URL. * - * This has a minimal handling of pluginfiles in order to generate a clean - * file ID which will not change if pointing to the same pluginfile URL even - * if the token or extra attributes have changed. + * This has a minimal handling of pluginfiles in order to generate a clean file ID which will not change if + * pointing to the same pluginfile URL even if the token or extra attributes have changed. + * + * The implementation of this function changed in version 2.9 to be able to have readable file names. + * The old implementation is in the function _getNonReadableFileIdByUrl. * * @module mm.core * @ngdoc method * @name $mmFilepool#_getFileIdByUrl * @param {String} fileUrl The absolute URL to the file. - * @return {Promise} The file ID. + * @return {Promise} Promise resolved with the file ID. * @protected */ self._getFileIdByUrl = function(fileUrl) { + var url = self._removeRevisionFromUrl(fileUrl), + filename, + candidate, + extension = ''; + + // Decode URL. + url = $mmText.decodeHTML(decodeURIComponent(url)); + + if (url.indexOf('/webservice/pluginfile') !== -1) { + // Remove attributes that do not matter. + angular.forEach(urlAttributes, function(regex) { + url = url.replace(regex, ''); + }); + } + + // Try to guess the filename the target file should have. We want to keep the original file name so + // people can easily identify the files after the download. + filename = self._guessFilenameFromUrl(url); + + // We need the extension for the inAppBrowser to open the files properly, e.g. the extension needs to be + // part of the file name. Also, we need the mimetype to open the file with web intents. The easiest way to + // provide such information is to keep the extension in the file ID. Developers should not care about it, + // but as we are using the file ID in the file path, devs and system can guess it. + candidate = self._guessExtensionFromUrl(url); + if (candidate && candidate !== 'php') { + extension = '.' + candidate; + } + + return filename + '_' + md5.createHash('url:' + url) + extension; + }; + + /** + * Old specification of _getFileIdByUrl. Creates a non readable fileId (hash). + * + * @module mm.core + * @ngdoc method + * @name $mmFilepool#_getNonReadableFileIdByUrl + * @param {String} fileUrl The absolute URL to the file. + * @return {String} The file ID. + * @protected + * @since 2.9 + */ + self._getNonReadableFileIdByUrl = function(fileUrl) { var url = self._removeRevisionFromUrl(fileUrl), candidate, extension = ''; @@ -1208,13 +1275,18 @@ angular.module('mm.core') var fileId, revision; - return self._fixPluginfileURL(siteId, fileUrl).then(function(fileUrl) { + return self._fixPluginfileURL(siteId, fileUrl).then(function(fixedUrl) { + fileUrl = fixedUrl; timemodified = timemodified || 0; revision = self.getRevisionFromUrl(fileUrl); - var fileId = self._getFileIdByUrl(fileUrl); + fileId = self._getFileIdByUrl(fileUrl); + + // Restore old file if needed. + return self._restoreOldFileIfNeeded(siteId, fileId, fileUrl); + }).then(function() { + return self._hasFileInPool(siteId, fileId).then(function(fileObject) { var response, - addToQueue = false, fn; if (typeof fileObject === 'undefined') { @@ -1316,11 +1388,16 @@ angular.module('mm.core') var fileId, revision; - return self._fixPluginfileURL(siteId, fileUrl).then(function(fileUrl) { + return self._fixPluginfileURL(siteId, fileUrl).then(function(fixedUrl) { + fileUrl = fixedUrl; timemodified = timemodified || 0; revision = self.getRevisionFromUrl(fileUrl); fileId = self._getFileIdByUrl(fileUrl); + // Restore old file if needed. + return self._restoreOldFileIfNeeded(siteId, fileId, fileUrl); + }).then(function() { + return self._hasFileInQueue(siteId, fileId).then(function() { return mmCoreDownloading; }, function() { @@ -1401,6 +1478,49 @@ angular.module('mm.core') return $q.reject(); }; + /** + * Get the path to a directory to store a package files. We use the old implementation of getFileId. + * + * This does not check if the file exists or not. + * + * @module mm.core + * @ngdoc method + * @name $mmFilepool#getPackageDirPathByUrl + * @param {String} siteId The site ID. + * @param {String} url An URL to identify the package. + * @return {Promise} Promise resolved with the path of the package. + * @since 2.9 + */ + self.getPackageDirPathByUrl = function(siteId, url) { + return self._fixPluginfileURL(siteId, url).then(function(fixedUrl) { + var fileId = self._getNonReadableFileIdByUrl(fixedUrl); + return self._getFilePath(siteId, fileId); + }); + }; + + /** + * Returns the local URL of a package directory. + * + * @module mm.core + * @ngdoc method + * @name $mmFilepool#getPackageDirUrlByUrl + * @param {String} siteId The site ID. + * @param {String} url An URL to identify the package. + * @return {Promise} Resolved with the URL. Rejected otherwise. + * @since 2.9 + */ + self.getPackageDirUrlByUrl = function(siteId, url) { + if ($mmFS.isAvailable()) { + return self._fixPluginfileURL(siteId, url).then(function(fixedUrl) { + var fileId = self._getNonReadableFileIdByUrl(fixedUrl); + return $mmFS.getDir(self._getFilePath(siteId, fileId)).then(function(dirEntry) { + return dirEntry.toURL(); + }); + }); + } + return $q.reject(); + }; + /** * Get package revision number from a list of files. * @@ -1534,6 +1654,57 @@ angular.module('mm.core') return extension; }; + /** + * Guess the filename of a file from its URL. + * + * This is very weak and unreliable. + * + * @module mm.core + * @ngdoc method + * @name $mmFilepool#_guessFilenameFromUrl + * @param {String} fileUrl The file URL. + * @return {String} The filename treated so it doesn't have any special character. + * @protected + * @since 2.9 + */ + self._guessFilenameFromUrl = function(fileUrl) { + var filename = ''; + + if (fileUrl.indexOf('/webservice/pluginfile') !== -1) { + // It's a pluginfile URL. Search for the 'file' param to extract the name. + var params = $mmUtil.extractUrlParams(fileUrl); + if (params.file) { + filename = params.file.substr(params.file.lastIndexOf('/') + 1); + } else { + // 'file' param not found. Extract what's after the last '/' without params. + filename = $mmText.getLastFileWithoutParams(fileUrl); + } + + } else if ($mmUtil.isGravatarUrl(fileUrl)) { + // Extract gravatar ID. + filename = 'gravatar_' + $mmText.getLastFileWithoutParams(fileUrl); + } else if ($mmUtil.isThemeImageUrl(fileUrl)) { + // Extract user ID. + var matches = fileUrl.match(/clean\/core\/([^\/]*)\//); + if (matches && matches[1]) { + filename = matches[1]; + } + // Attach a constant and the image type. + filename = 'default_' + filename + '_' + $mmText.getLastFileWithoutParams(fileUrl); + } else { + // Another URL. Just get what's after the last /. + filename = $mmText.getLastFileWithoutParams(fileUrl); + } + + // Remove the extension from the filename. + var position = filename.lastIndexOf('.'); + if (position != -1) { + filename = filename.substr(0, position); + } + + return $mmText.removeSpecialCharactersForFiles(filename); + }; + /** * Invalidate all the files in a site. * @@ -1833,75 +2004,78 @@ angular.module('mm.core') * Download helper to avoid code duplication. */ function download(siteId, fileUrl, fileObject, links) { - return self._downloadForPoolByUrl(siteId, fileUrl, revision, timemodified, filePath, fileObject).then(function() { - var promise; - - // Success, we add links and remove from queue. - self._addFileLinks(siteId, fileId, links); - promise = self._removeFromQueue(siteId, fileId); - - self._treatQueueDeferred(siteId, fileId, true); - self._notifyFileDownloaded(siteId, fileId); - - // Wait for the item to be removed from queue before resolving the promise. - // If the item could not be removed from queue we still resolve the promise. - return promise.catch(function() {}); - - }, function(errorObject) { - // Whoops, we have an error... - var dropFromQueue = false; - - if (typeof errorObject !== 'undefined' && errorObject.source === fileUrl) { - // This is most likely a $cordovaFileTransfer error. + // Restore old file if needed. + return self._restoreOldFileIfNeeded(siteId, fileId, fileUrl, filePath).then(function() { + return self._downloadForPoolByUrl(siteId, fileUrl, revision, timemodified, filePath, fileObject).then(function() { + var promise; - if (errorObject.code === 1) { // FILE_NOT_FOUND_ERR. - // The file was not found, most likely a 404, we remove from queue. - dropFromQueue = true; + // Success, we add links and remove from queue. + self._addFileLinks(siteId, fileId, links); + promise = self._removeFromQueue(siteId, fileId); - } else if (errorObject.code === 2) { // INVALID_URL_ERR. - // The URL is invalid, we drop the file from the queue. - dropFromQueue = true; + self._treatQueueDeferred(siteId, fileId, true); + self._notifyFileDownloaded(siteId, fileId); - } else if (errorObject.code === 3) { // CONNECTION_ERR. - // If there was an HTTP status, then let's remove from the queue. - dropFromQueue = true; - } else if (errorObject.code === 4) { // ABORTED_ERR. - // The transfer was aborted, we will keep the file in queue. - } else if (errorObject.code === 5) { // NOT_MODIFIED_ERR. - // We have the latest version of the file, HTTP 304 status. - dropFromQueue = true; + // Wait for the item to be removed from queue before resolving the promise. + // If the item could not be removed from queue we still resolve the promise. + return promise.catch(function() {}); + + }, function(errorObject) { + // Whoops, we have an error... + var dropFromQueue = false; + + if (typeof errorObject !== 'undefined' && errorObject.source === fileUrl) { + // This is most likely a $cordovaFileTransfer error. + + if (errorObject.code === 1) { // FILE_NOT_FOUND_ERR. + // The file was not found, most likely a 404, we remove from queue. + dropFromQueue = true; + + } else if (errorObject.code === 2) { // INVALID_URL_ERR. + // The URL is invalid, we drop the file from the queue. + dropFromQueue = true; + + } else if (errorObject.code === 3) { // CONNECTION_ERR. + // If there was an HTTP status, then let's remove from the queue. + dropFromQueue = true; + } else if (errorObject.code === 4) { // ABORTED_ERR. + // The transfer was aborted, we will keep the file in queue. + } else if (errorObject.code === 5) { // NOT_MODIFIED_ERR. + // We have the latest version of the file, HTTP 304 status. + dropFromQueue = true; + } else { + // Unknown error, let's remove the file from the queue to avoid + // locking down the queue because of one file. + dropFromQueue = true; + } } else { - // Unknown error, let's remove the file from the queue to avoid - // locking down the queue because of one file. dropFromQueue = true; } - } else { - dropFromQueue = true; - } - if (dropFromQueue) { - var promise; + if (dropFromQueue) { + var promise; - $log.debug('Item dropped from queue due to error: ' + fileUrl); - promise = self._removeFromQueue(siteId, fileId); + $log.debug('Item dropped from queue due to error: ' + fileUrl); + promise = self._removeFromQueue(siteId, fileId); - // Consider this as a silent error, never reject the promise here. - return promise.catch(function() {}).finally(function() { + // Consider this as a silent error, never reject the promise here. + return promise.catch(function() {}).finally(function() { + self._treatQueueDeferred(siteId, fileId, false); + self._notifyFileDownloadError(siteId, fileId); + }); + } else { + // We considered the file as legit but did not get it, failure. self._treatQueueDeferred(siteId, fileId, false); self._notifyFileDownloadError(siteId, fileId); - }); - } else { - // We considered the file as legit but did not get it, failure. - self._treatQueueDeferred(siteId, fileId, false); - self._notifyFileDownloadError(siteId, fileId); - return $q.reject(); - } + return $q.reject(); + } - }, function(progress) { - // Send the progress object to the queue deferred. - if (queueDeferreds[siteId] && queueDeferreds[siteId][fileId]) { - queueDeferreds[siteId][fileId].notify(progress); - } + }, function(progress) { + // Send the progress object to the queue deferred. + if (queueDeferreds[siteId] && queueDeferreds[siteId][fileId]) { + queueDeferreds[siteId][fileId].notify(progress); + } + }); }); } @@ -1988,7 +2162,11 @@ angular.module('mm.core') self.removeFileByUrl = function(siteId, fileUrl) { return self._fixPluginfileURL(siteId, fileUrl).then(function(fileUrl) { var fileId = self._getFileIdByUrl(fileUrl); - return self._removeFileById(siteId, fileId); + + // Restore old file if needed. + return self._restoreOldFileIfNeeded(siteId, fileId, fileUrl).then(function() { + return self._removeFileById(siteId, fileId); + }); }); }; @@ -2008,6 +2186,63 @@ angular.module('mm.core') return url.replace(revisionRegex, '/content/0/'); }; + /** + * The way to create the file ID changed in 2.9 to keep the original filename (see MOBILE-1408). + * To prevent losing files already downloaded, this function will try to move files using old fileId to new fileId. + * + * @module mm.core + * @ngdoc method + * @name $mmFilepool#_restoreOldFileIfNeeded + * @param {String} siteId Site ID. + * @param {String} fileId File's new ID. + * @param {String} fileUrl File URL. + * @param {String} [filePath] Filepath to download the file to (for packages). + * @return {Promise} Promise resolved when done. It's never rejected. + * @protected + */ + self._restoreOldFileIfNeeded = function(siteId, fileId, fileUrl, filePath) { + var fileObject, + oldFileId = self._getNonReadableFileIdByUrl(fileUrl); + + if (fileId == oldFileId) { + // Same ID, nothing to do. + return $q.when(); + } + + // Check that the new file isn't in pool. + return self._hasFileInPool(siteId, fileId).catch(function() { + // Not in pool. Check that old file is in pool. + return self._hasFileInPool(siteId, oldFileId).then(function(entry) { + fileObject = entry; + + if (filePath) { + // File path is set, no need to move the file because path hasn't changed. + return $q.when(); + } else { + // Old file is in pool. Copy the file using the new ID. + return $mmFS.copyFile(self._getFilePath(siteId, oldFileId), self._getFilePath(siteId, fileId)); + } + }).then(function() { + // File copied. Update the entry in the pool. + return self._addFileToPool(siteId, fileId, fileObject); + }).then(function() { + // Filepool updated. Now updated links. + return self._getFileLinks(siteId, fileId).then(function(links) { + var promises = []; + angular.forEach(links, function(link) { + promises.push(self._addFileLink(siteId, fileId, link.component, link.componentId)); + }); + return $q.all(promises); + }); + }).then(function() { + // Everything has been moved. Delete old entries. + return self._removeFileById(siteId, oldFileId); + }).catch(function() { + // Ignore errors. + }); + }); + }; + /** * Change the package status, setting it to the previous status. * diff --git a/www/core/lib/fs.js b/www/core/lib/fs.js index 7c479c6abba..fb796cb2fc3 100644 --- a/www/core/lib/fs.js +++ b/www/core/lib/fs.js @@ -692,15 +692,40 @@ angular.module('mm.core') */ self.copyFile = function(from, to) { return self.init().then(function() { - // Check if to contains a directory. - var toFile = self.getFileAndDirectoryFromPath(to); - if (toFile.directory == '') { - return $cordovaFile.copyFile(basePath, from, basePath, to); + if (isHTMLAPI) { + // In Cordova API we need to calculate the longest matching path to make it work. + // $cordovaFile.copyFile('a/', 'b/c.ext', 'a/', 'b/d.ext') doesn't work. + // cordovaFile.copyFile('a/b/', 'c.ext', 'a/b/', 'd.ext') works. + var commonPath = basePath, + dirsA = from.split('/'), + dirsB = to.split('/'); + + for (var i = 0; i < dirsA.length; i++) { + var dir = dirsA[i]; + if (dirsB[i] === dir) { + // Found a common folder, add it to common path and remove it from each specific path. + dir = dir + '/'; + commonPath = self.concatenatePaths(commonPath, dir); + from = from.replace(dir, ''); + to = to.replace(dir, ''); + } else { + // Folder doesn't match, stop searching. + break; + } + } + + return $cordovaFile.copyFile(commonPath, from, commonPath, to); } else { - // Ensure directory is created. - return self.createDir(toFile.directory).then(function() { + // Check if to contains a directory. + var toFile = self.getFileAndDirectoryFromPath(to); + if (toFile.directory == '') { return $cordovaFile.copyFile(basePath, from, basePath, to); - }); + } else { + // Ensure directory is created. + return self.createDir(toFile.directory).then(function() { + return $cordovaFile.copyFile(basePath, from, basePath, to); + }); + } } }); }; diff --git a/www/core/lib/text.js b/www/core/lib/text.js index 9de70e97320..953eda3341d 100644 --- a/www/core/lib/text.js +++ b/www/core/lib/text.js @@ -204,6 +204,31 @@ angular.module('mm.core') .replace(/'/g, "'"); }; + /** + * Decode an escaped HTML text. This implementation is based on PHP's htmlspecialchars_decode. + * + * @module mm.core + * @ngdoc method + * @name $mmText#decodeHTML + * @param {String} text Text to decode. + * @return {String} Decoded text. + */ + self.decodeHTML = function(text) { + if (typeof text == 'undefined' || text === null || (typeof text == 'number' && isNaN(text))) { + return ''; + } else if (typeof text != 'string') { + return '' + text; + } + + return text + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, ' '); + }; + /** * Add or remove 'www' from a URL. The url needs to have http or https protocol. * @@ -265,5 +290,37 @@ angular.module('mm.core') } }; + /** + * Replace all characters that cause problems with files in Android and iOS. + * + * @module mm.core + * @ngdoc method + * @name $mmText#removeSpecialCharactersForFiles + * @param {String} text Text to treat. + * @return {String} Treated text. + */ + self.removeSpecialCharactersForFiles = function(text) { + return text.replace(/[#:\/\?\\]+/g, '_'); + }; + + /** + * Given a URL, returns what's after the last '/' without params. + * Example: + * http://mysite.com/a/course.html?id=1 -> course.html + * + * @module mm.core + * @ngdoc method + * @name $mmText#getLastFileWithoutParams + * @param {String} url URL to treat. + * @return {String} Last file without params. + */ + self.getLastFileWithoutParams = function(url) { + var filename = url.substr(url.lastIndexOf('/') + 1); + if (filename.indexOf('?') != -1) { + filename = filename.substr(0, filename.indexOf('?')); + } + return filename; + }; + return self; }); From 5aadbd1d3713a9344ac7d4df4076c4b7746c1fd4 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 23 Feb 2016 13:56:07 +0100 Subject: [PATCH 2/2] MOBILE-1408 filepool: Fix download errors in iOS because of some chars --- www/core/lib/emulator.js | 3 +++ www/core/lib/fs.js | 11 ++++++----- www/core/lib/ws.js | 22 ++++++++++++---------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/www/core/lib/emulator.js b/www/core/lib/emulator.js index 8ff8964a776..7e610270a04 100644 --- a/www/core/lib/emulator.js +++ b/www/core/lib/emulator.js @@ -65,6 +65,7 @@ angular.module('mm.core') errorCallback(); } else { filePath = filePath.replace(basePath, ''); // Remove basePath from the filePath. + filePath = filePath.replace(/%20/g, ' '); // Replace all %20 with spaces. $mmFS.writeFile(filePath, data.data).then(function(e) { successCallback(e); }).catch(function(error) { @@ -81,7 +82,9 @@ angular.module('mm.core') unzip: function(source, destination, callback, progressCallback) { // Remove basePath from the source and destination. source = source.replace(basePath, ''); + source = source.replace(/%20/g, ' '); // Replace all %20 with spaces. destination = destination.replace(basePath, ''); + destination = destination.replace(/%20/g, ' '); // Replace all %20 with spaces. $mmFS.readFile(source, $mmFS.FORMATARRAYBUFFER).then(function(data) { var zip = new JSZip(data), diff --git a/www/core/lib/fs.js b/www/core/lib/fs.js index fb796cb2fc3..67f602a8563 100644 --- a/www/core/lib/fs.js +++ b/www/core/lib/fs.js @@ -914,11 +914,12 @@ angular.module('mm.core') * @return {Promise} Promise resolved when the file is unzipped. */ self.unzipFile = function(path, destFolder) { - // We need to use ansolute paths (including basePath). - path = self.addBasePathIfNeeded(path); - // If destFolder is not set, use same location as ZIP file. - destFolder = self.addBasePathIfNeeded(destFolder || self.removeExtension(path)); - return $cordovaZip.unzip(path, destFolder); + // Get the source file. + return self.getFile(path).then(function(fileEntry) { + // If destFolder is not set, use same location as ZIP file. We need to use ansolute paths (including basePath). + destFolder = self.addBasePathIfNeeded(destFolder || self.removeExtension(path)); + return $cordovaZip.unzip(fileEntry.toURL(), destFolder); + }); }; return self; diff --git a/www/core/lib/ws.js b/www/core/lib/ws.js index a03db019296..ebb88619c2a 100644 --- a/www/core/lib/ws.js +++ b/www/core/lib/ws.js @@ -148,20 +148,22 @@ angular.module('mm.core') self.downloadFile = function(url, path, background) { $log.debug('Downloading file ' + url); - return $mmFS.getBasePathToDownload().then(function(basePath) { - // Use a tmp path to download the file and then move it to final location. This is because if the download fails, - // the local file is deleted. - var tmpPath = basePath + path + '.tmp'; - return $cordovaFileTransfer.download(url, tmpPath, { encodeURI: false }, true).then(function() { - return $mmFS.moveFile(path + '.tmp', path).then(function(movedEntry) { + // Use a tmp path to download the file and then move it to final location.This is because if the download fails, + // the local file is deleted. + var tmpPath = path + '.tmp'; + + // Create the tmp file as an empty file. + return $mmFS.createFile(tmpPath).then(function(fileEntry) { + return $cordovaFileTransfer.download(url, fileEntry.toURL(), { encodeURI: false }, true).then(function() { + return $mmFS.moveFile(tmpPath, path).then(function(movedEntry) { $log.debug('Success downloading file ' + url + ' to ' + path); return movedEntry; }); - }, function(err) { - $log.error('Error downloading ' + url + ' to ' + path); - $log.error(JSON.stringify(err)); - return $q.reject(err); }); + }).catch(function(err) { + $log.error('Error downloading ' + url + ' to ' + path); + $log.error(JSON.stringify(err)); + return $q.reject(err); }); };