From 55393cd1ee1d0c64ddf037791e47eb43549cb22e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 23 Jan 2018 15:29:00 +0100 Subject: [PATCH] MOBILE-2322 files: Create providers and handler --- src/addon/files/files.module.ts | 36 ++ src/addon/files/lang/en.json | 8 + src/addon/files/providers/files.ts | 415 ++++++++++++++++++ src/addon/files/providers/helper.ts | 74 ++++ src/addon/files/providers/mainmenu-handler.ts | 51 +++ src/app/app.module.ts | 4 +- 6 files changed, 587 insertions(+), 1 deletion(-) create mode 100644 src/addon/files/files.module.ts create mode 100644 src/addon/files/lang/en.json create mode 100644 src/addon/files/providers/files.ts create mode 100644 src/addon/files/providers/helper.ts create mode 100644 src/addon/files/providers/mainmenu-handler.ts diff --git a/src/addon/files/files.module.ts b/src/addon/files/files.module.ts new file mode 100644 index 000000000..56560abc2 --- /dev/null +++ b/src/addon/files/files.module.ts @@ -0,0 +1,36 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { AddonFilesProvider } from './providers/files'; +import { AddonFilesHelperProvider } from './providers/helper'; +import { AddonFilesMainMenuHandler } from './providers/mainmenu-handler'; +import { CoreMainMenuDelegate } from '../../core/mainmenu/providers/delegate'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + AddonFilesProvider, + AddonFilesHelperProvider, + AddonFilesMainMenuHandler + ] +}) +export class AddonFilesModule { + constructor(mainMenuDelegate: CoreMainMenuDelegate, filesHandler: AddonFilesMainMenuHandler) { + mainMenuDelegate.registerHandler(filesHandler); + } +} diff --git a/src/addon/files/lang/en.json b/src/addon/files/lang/en.json new file mode 100644 index 000000000..6f0e863a2 --- /dev/null +++ b/src/addon/files/lang/en.json @@ -0,0 +1,8 @@ +{ + "couldnotloadfiles": "The list of files could not be loaded.", + "emptyfilelist": "There are no files to show.", + "erroruploadnotworking": "Unfortunately it is currently not possible to upload files to your site.", + "files": "My files", + "privatefiles": "Private files", + "sitefiles": "Site files" +} \ No newline at end of file diff --git a/src/addon/files/providers/files.ts b/src/addon/files/providers/files.ts new file mode 100644 index 000000000..268a835a3 --- /dev/null +++ b/src/addon/files/providers/files.ts @@ -0,0 +1,415 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreMimetypeUtilsProvider } from '../../../providers/utils/mimetype'; +import { CoreSite } from '../../../classes/site'; +import { Md5 } from 'ts-md5/dist/md5'; + +/** + * Service to handle my files and site files. + */ +@Injectable() +export class AddonFilesProvider { + protected ROOT_CACHE_KEY = 'mmaFiles:'; + static PRIVATE_FILES_COMPONENT = 'mmaFilesMy'; + static SITE_FILES_COMPONENT = 'mmaFilesSite'; + + constructor(private sitesProvider: CoreSitesProvider, private mimeUtils: CoreMimetypeUtilsProvider) { } + + /** + * Check if core_user_get_private_files_info WS call is available. + * + * @return {boolean} Whether the WS is available, false otherwise. + */ + canGetPrivateFilesInfo(): boolean { + return this.sitesProvider.getCurrentSite().wsAvailable('core_user_get_private_files_info'); + } + + /** + * Check if user can view his private files. + * + * @return {boolean} Whether the user can view his private files. + */ + canViewPrivateFiles(): boolean { + return this.sitesProvider.getCurrentSite().canAccessMyFiles() && !this.isPrivateFilesDisabledInSite(); + } + + /** + * Check if user can view site files. + * + * @return {boolean} Whether the user can view site files. + */ + canViewSiteFiles(): boolean { + return !this.isSiteFilesDisabledInSite(); + } + + /** + * Check if user can upload private files. + * + * @return {boolean} Whether the user can upload private files. + */ + canUploadFiles(): boolean { + const currentSite = this.sitesProvider.getCurrentSite(); + + return currentSite.canAccessMyFiles() && currentSite.canUploadFiles() && !this.isUploadDisabledInSite(); + } + + /** + * Get the list of files. + * + * @param {any} params A list of parameters accepted by the Web service. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the files. + */ + getFiles(params: any, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + const preSets = { + cacheKey: this.getFilesListCacheKey(params) + }; + + return site.read('core_files_get_files', params, preSets); + }).then((result) => { + const entries = []; + + if (result.files) { + result.files.forEach((entry) => { + if (entry.isdir) { + // Create a "link" to load the folder. + entry.link = { + contextid: entry.contextid || '', + component: entry.component || '', + filearea: entry.filearea || '', + itemid: entry.itemid || 0, + filepath: entry.filepath || '', + filename: entry.filename || '' + }; + + if (entry.component) { + // Delete unused elements that may break the request. + entry.link.filename = ''; + } + } + + if (entry.isdir) { + entry.imgPath = this.mimeUtils.getFolderIcon(); + } else { + entry.imgPath = this.mimeUtils.getFileIcon(entry.filename); + } + + entries.push(entry); + }); + } + + return entries; + }); + } + + /** + * Get cache key for file list WS calls. + * + * @param {any} params Params of the WS. + * @return {string} Cache key. + */ + protected getFilesListCacheKey(params: any): string { + const root = !params.component ? 'site' : 'my'; + + return this.ROOT_CACHE_KEY + 'list:' + root + ':' + params.contextid + ':' + params.filepath; + } + + /** + * Get the private files of the current user. + * + * @return {Promise} Promise resolved with the files. + */ + getPrivateFiles(): Promise { + return this.getFiles(this.getPrivateFilesRootParams()); + } + + /** + * Get params to get root private files directory. + * + * @return {any} Params. + */ + protected getPrivateFilesRootParams(): any { + return { + contextid: -1, + component: 'user', + filearea: 'private', + contextlevel: 'user', + instanceid: this.sitesProvider.getCurrentSite().getUserId(), + itemid: 0, + filepath: '', + filename: '' + }; + } + + /** + * Get private files info. + * + * @param {number} [userId] User ID. If not defined, current user in the site. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved with the info. + */ + getPrivateFilesInfo(userId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + const params = { + userid: userId + }, + preSets = { + cacheKey: this.getPrivateFilesInfoCacheKey(userId) + }; + + return site.read('core_user_get_private_files_info', params, preSets); + }); + } + + /** + * Get the cache key for private files info WS calls. + * + * @param {number} userId User ID. + * @return {string} Cache key. + */ + protected getPrivateFilesInfoCacheKey(userId: number): string { + return this.getPrivateFilesInfoCommonCacheKey() + ':' + userId; + } + + /** + * Get the common part of the cache keys for private files info WS calls. + * + * @return {string} Cache key. + */ + protected getPrivateFilesInfoCommonCacheKey(): string { + return this.ROOT_CACHE_KEY + 'privateInfo'; + } + + /** + * Get the site files. + * + * @return {Promise} Promise resolved with the files. + */ + getSiteFiles(): Promise { + return this.getFiles(this.getSiteFilesRootParams()); + } + + /** + * Get params to get root site files directory. + * + * @return {any} Params. + */ + protected getSiteFilesRootParams(): any { + return { + contextid: 0, + component: '', + filearea: '', + itemid: 0, + filepath: '', + filename: '' + }; + } + + /** + * Invalidates list of files in a certain directory. + * + * @param {string} root Root of the directory ('my' for private files, 'site' for site files). + * @param {string} path Path to the directory. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateDirectory(root: string, path: string, siteId?: string): Promise { + let params; + if (!path) { + if (root === 'site') { + params = this.getSiteFilesRootParams(); + } else if (root === 'my') { + params = this.getPrivateFilesRootParams(); + } + } else { + params = path; + } + + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getFilesListCacheKey(params)); + }); + } + + /** + * Invalidates private files info for all users. + * + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidatePrivateFilesInfo(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getPrivateFilesInfoCommonCacheKey()); + }); + } + + /** + * Invalidates private files info for a certain user. + * + * @param {number} [userId] User ID. If not defined, current user in the site. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidatePrivateFilesInfoForUser(userId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return site.invalidateWsCacheForKey(this.getPrivateFilesInfoCacheKey(userId)); + }); + } + + /** + * Check if Files is disabled in a certain site. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + isDisabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isDisabledInSite(site); + }); + } + + /** + * Check if Files is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isDisabledInSite(site: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('$mmSideMenuDelegate_mmaFiles'); + } + + /** + * Return whether or not the plugin is enabled. + * + * @return {boolean} True if enabled, false otherwise. + */ + isPluginEnabled(): boolean { + return this.canViewPrivateFiles() || this.canViewSiteFiles() || this.canUploadFiles(); + } + + /** + * Check if private files is disabled in a certain site. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + isPrivateFilesDisabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isPrivateFilesDisabledInSite(site); + }); + } + + /** + * Check if private files is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isPrivateFilesDisabledInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('files_privatefiles'); + } + + /** + * Check if site files is disabled in a certain site. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + isSiteFilesDisabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isSiteFilesDisabledInSite(site); + }); + } + + /** + * Check if site files is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isSiteFilesDisabledInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('files_sitefiles'); + } + + /** + * Check if upload files is disabled in a certain site. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + isUploadDisabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isUploadDisabledInSite(site); + }); + } + + /** + * Check if upload files is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isUploadDisabledInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('files_upload'); + } + + /** + * Move a file from draft area to private files. + * + * @param {number} draftId The draft area ID of the file. + * @param {string} [siteid] ID of the site. If not defined, use current site. + * @return {Promise} Promise resolved in success, rejected otherwise. + */ + moveFromDraftToPrivate(draftId: number, siteId?: string): Promise { + const params = { + draftid: draftId + }, + preSets = { + responseExpected: false + }; + + return this.sitesProvider.getSite(siteId).then((site) => { + return site.write('core_user_add_user_private_files', params, preSets); + }); + } + + /** + * Check the Moodle version in order to check if upload files is working. + * + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved with true if WS is working, false otherwise. + */ + versionCanUploadFiles(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // Upload private files doesn't work for Moodle 3.1.0 due to a bug. + return site.isVersionGreaterEqualThan('3.1.1'); + }); + } +} diff --git a/src/addon/files/providers/helper.ts b/src/addon/files/providers/helper.ts new file mode 100644 index 000000000..467cb0ad9 --- /dev/null +++ b/src/addon/files/providers/helper.ts @@ -0,0 +1,74 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; +import { CoreFileUploaderHelperProvider } from '../../../core/fileuploader/providers/helper'; +import { AddonFilesProvider } from './files'; + +/** + * Service that provides some features regarding private and site files. + */ +@Injectable() +export class AddonFilesHelperProvider { + + constructor(private sitesProvider: CoreSitesProvider, private fileUploaderHelper: CoreFileUploaderHelperProvider, + private filesProvider: AddonFilesProvider, private domUtils: CoreDomUtilsProvider) { } + + /** + * Select a file, upload it and move it to private files. + * + * @param {any} [info] Private files info. See AddonFilesProvider.getPrivateFilesInfo. + * @return {Promise} Promise resolved when a file is uploaded, rejected otherwise. + */ + uploadPrivateFile(info?: any): Promise { + // Calculate the max size. + const currentSite = this.sitesProvider.getCurrentSite(); + let maxSize = currentSite.getInfo().usermaxuploadfilesize, + userQuota = currentSite.getInfo().userquota; + + if (userQuota === 0) { + // 0 means ignore user quota. In the app it is -1. + userQuota = -1; + } else if (userQuota > 0 && typeof info != 'undefined') { + userQuota = userQuota - info.filesizewithoutreferences; + } + + if (typeof userQuota != 'undefined') { + // Use the minimum value. + maxSize = Math.min(maxSize, userQuota); + } + + // Select and upload the file. + return this.fileUploaderHelper.selectAndUploadFile(maxSize).then((result) => { + if (!result) { + return Promise.reject(null); + } + + // File uploaded. Move it to private files. + const modal = this.domUtils.showModalLoading('core.fileuploader.uploading', true); + + return this.filesProvider.moveFromDraftToPrivate(result.itemid).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.fileuploader.errorwhileuploading', true); + + return Promise.reject(null); + }).finally(() => { + modal.dismiss(); + }); + }).then(() => { + this.domUtils.showAlertTranslated('core.success', 'core.fileuploader.fileuploaded'); + }); + } +} diff --git a/src/addon/files/providers/mainmenu-handler.ts b/src/addon/files/providers/mainmenu-handler.ts new file mode 100644 index 000000000..fb7e82d71 --- /dev/null +++ b/src/addon/files/providers/mainmenu-handler.ts @@ -0,0 +1,51 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { AddonFilesProvider } from './files'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../../core/mainmenu/providers/delegate'; + +/** + * Handler to inject an option into main menu. + */ +@Injectable() +export class AddonFilesMainMenuHandler implements CoreMainMenuHandler { + name = 'AddonFiles'; + priority = 200; + + constructor(private filesProvider: AddonFilesProvider) { } + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return this.filesProvider.isPluginEnabled(); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreMainMenuHandlerData} Data needed to render the handler. + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + icon: 'folder', + title: 'addon.files.files', + page: 'AddonFilesListPage', + class: 'addon-files-handler' + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3d3620622..d96ef144c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -65,6 +65,7 @@ import { CoreUserModule } from '../core/user/user.module'; // Addon modules. import { AddonCalendarModule } from '../addon/calendar/calendar.module'; import { AddonUserProfileFieldModule } from '../addon/userprofilefield/userprofilefield.module'; +import { AddonFilesModule } from '../addon/files/files.module'; // For translate loader. AoT requires an exported function for factories. export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { @@ -101,7 +102,8 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { CoreContentLinksModule, CoreUserModule, AddonCalendarModule, - AddonUserProfileFieldModule + AddonUserProfileFieldModule, + AddonFilesModule ], bootstrap: [IonicApp], entryComponents: [