From 310ee19d26648789e23c6ba0438079b44198376b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 22 Jan 2021 11:58:20 +0100 Subject: [PATCH] MOBILE-3659 course: Implement course sync --- src/core/classes/errors/network-error.ts | 26 ++ src/core/features/course/course.module.ts | 6 + .../course/pages/contents/contents.ts | 32 +-- .../features/course/services/course-helper.ts | 5 +- .../course/services/handlers/log-cron.ts | 65 +++++ .../course/services/handlers/sync-cron.ts | 52 ++++ src/core/features/course/services/sync.ts | 260 ++++++++++++++++++ src/core/services/file-helper.ts | 3 +- src/core/services/sites.ts | 5 +- src/core/services/utils/dom.ts | 9 +- src/core/services/ws.ts | 9 +- 11 files changed, 444 insertions(+), 28 deletions(-) create mode 100644 src/core/classes/errors/network-error.ts create mode 100644 src/core/features/course/services/handlers/log-cron.ts create mode 100644 src/core/features/course/services/handlers/sync-cron.ts create mode 100644 src/core/features/course/services/sync.ts diff --git a/src/core/classes/errors/network-error.ts b/src/core/classes/errors/network-error.ts new file mode 100644 index 000000000..c59c21045 --- /dev/null +++ b/src/core/classes/errors/network-error.ts @@ -0,0 +1,26 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { Translate } from '@singletons'; + +/** + * Network error. It will automatically set the right error message if none is provided. + */ +export class CoreNetworkError extends Error { + + constructor(message?: string) { + super(message || Translate.instance.instant('core.networkerrormsg')); + } + +} diff --git a/src/core/features/course/course.module.ts b/src/core/features/course/course.module.ts index e8bf37e7d..bd623076c 100644 --- a/src/core/features/course/course.module.ts +++ b/src/core/features/course/course.module.ts @@ -25,6 +25,9 @@ import { SITE_SCHEMA as LOG_SITE_SCHEMA } from './services/database/log'; import { SITE_SCHEMA as PREFETCH_SITE_SCHEMA } from './services/database/module-prefetch'; import { CoreCourseIndexRoutingModule } from './pages/index/index-routing.module'; import { CoreCourseModulePrefetchDelegate } from './services/module-prefetch-delegate'; +import { CoreCronDelegate } from '@services/cron'; +import { CoreCourseLogCronHandler } from './services/handlers/log-cron'; +import { CoreCourseSyncCronHandler } from './services/handlers/sync-cron'; const routes: Routes = [ { @@ -60,6 +63,9 @@ const courseIndexRoutes: Routes = [ multi: true, deps: [], useFactory: () => () => { + CoreCronDelegate.instance.register(CoreCourseSyncCronHandler.instance); + CoreCronDelegate.instance.register(CoreCourseLogCronHandler.instance); + CoreCourseModulePrefetchDelegate.instance.initialize(); }, }, diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index a3781a086..d0fea2104 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -36,7 +36,7 @@ import { CoreCourseOptionsDelegate, CoreCourseOptionsMenuHandlerToDisplay, } from '@features/course/services/course-options-delegate'; -// import { CoreCourseSyncProvider } from '../../providers/sync'; +import { CoreCourseAutoSyncData, CoreCourseSync, CoreCourseSyncProvider } from '@features/course/services/sync'; import { CoreCourseFormatComponent } from '../../components/format/format'; import { CoreEvents, @@ -144,15 +144,17 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { }, ); - // @todo this.syncObserver = CoreEvents.on(CoreCourseSyncProvider.AUTO_SYNCED, (data) => { - // if (data && data.courseId == this.course.id) { - // this.refreshAfterCompletionChange(false); + this.syncObserver = CoreEvents.on(CoreCourseSyncProvider.AUTO_SYNCED, (data) => { + if (!data || data.courseId != this.course.id) { + return; + } - // if (data.warnings && data.warnings[0]) { - // CoreDomUtils.instance.showErrorModal(data.warnings[0]); - // } - // } - // }); + this.refreshAfterCompletionChange(false); + + if (data.warnings && data.warnings[0]) { + CoreDomUtils.instance.showErrorModal(data.warnings[0]); + } + }); } /** @@ -210,13 +212,11 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { if (sync) { // Try to synchronize the course data. - // @todo return this.syncProvider.syncCourse(this.course.id).then((result) => { - // if (result.warnings && result.warnings.length) { - // CoreDomUtils.instance.showErrorModal(result.warnings[0]); - // } - // }).catch(() => { - // // For now we don't allow manual syncing, so ignore errors. - // }); + // For now we don't allow manual syncing, so ignore errors. + const result = await CoreUtils.instance.ignoreErrors(CoreCourseSync.instance.syncCourse(this.course.id)); + if (result?.warnings?.length) { + CoreDomUtils.instance.showErrorModal(result.warnings[0]); + } } try { diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index fdf595792..3eb36e61b 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -63,6 +63,7 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreEventObserver, CoreEventPackageStatusChanged, CoreEvents } from '@singletons/events'; import { CoreFilterHelper } from '@features/filter/services/filter-helper'; +import { CoreNetworkError } from '@classes/errors/network-error'; /** * Prefetch info of a module. @@ -732,7 +733,7 @@ export class CoreCourseHelperProvider { try { path = await CoreFilepool.instance.getInternalUrlByUrl(site.getId(), fileUrl); } catch { - throw new CoreError(Translate.instance.instant('core.networkerrormsg')); + throw new CoreNetworkError(); } return CoreUtils.instance.openFile(path); @@ -857,7 +858,7 @@ export class CoreCourseHelperProvider { if (!isOnline && status === CoreConstants.NOT_DOWNLOADED) { // Not downloaded and we're offline, reject. - throw new CoreError(Translate.instance.instant('core.networkerrormsg')); + throw new CoreNetworkError(); } const shouldDownloadFirst = await CoreFilepool.instance.shouldDownloadFileBeforeOpen(fixedUrl, mainFile.filesize); diff --git a/src/core/features/course/services/handlers/log-cron.ts b/src/core/features/course/services/handlers/log-cron.ts new file mode 100644 index 000000000..9940cf672 --- /dev/null +++ b/src/core/features/course/services/handlers/log-cron.ts @@ -0,0 +1,65 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreCronHandler } from '@services/cron'; +import { CoreSites } from '@services/sites'; +import { CoreCourse } from '@features/course/services/course'; +import { makeSingleton } from '@singletons'; + +/** + * Log cron handler. It will update last access of the user while app is open. + */ +@Injectable({ providedIn: 'root' }) +export class CoreCourseLogCronHandlerService implements CoreCronHandler { + + name = 'CoreCourseLogCronHandler'; + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param siteId ID of the site affected, undefined for all sites. + * @param force Wether the execution is forced (manual sync). + * @return Promise resolved when done, rejected if failure. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async execute(siteId?: string, force?: boolean): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return CoreCourse.instance.logView(site.getSiteHomeId(), undefined, site.getId(), site.getInfo()?.sitename); + } + + /** + * Check whether it's a synchronization process or not. + * + * @return Whether it's a synchronization process or not. + */ + isSync(): boolean { + return false; + } + + /** + * Get the time between consecutive executions. + * + * @return Time between consecutive executions (in ms). + */ + getInterval(): number { + return 240000; // 4 minutes. By default platform will see the user as online if lastaccess is less than 5 minutes. + } + +} + +export class CoreCourseLogCronHandler extends makeSingleton(CoreCourseLogCronHandlerService) {} diff --git a/src/core/features/course/services/handlers/sync-cron.ts b/src/core/features/course/services/handlers/sync-cron.ts new file mode 100644 index 000000000..39a96a82d --- /dev/null +++ b/src/core/features/course/services/handlers/sync-cron.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreCronHandler } from '@services/cron'; +import { makeSingleton } from '@singletons'; +import { CoreCourseSync } from '../sync'; + +/** + * Synchronization cron handler. + */ +@Injectable({ providedIn: 'root' }) +export class CoreCourseSyncCronHandlerService implements CoreCronHandler { + + name = 'CoreCourseSyncCronHandler'; + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param siteId ID of the site affected, undefined for all sites. + * @param force Wether the execution is forced (manual sync). + * @return Promise resolved when done, rejected if failure. + */ + execute(siteId?: string, force?: boolean): Promise { + return CoreCourseSync.instance.syncAllCourses(siteId, force); + } + + /** + * Get the time between consecutive executions. + * + * @return Time between consecutive executions (in ms). + */ + getInterval(): number { + return CoreCourseSync.instance.syncInterval; + } + +} + +export class CoreCourseSyncCronHandler extends makeSingleton(CoreCourseSyncCronHandlerService) {} diff --git a/src/core/features/course/services/sync.ts b/src/core/features/course/services/sync.ts new file mode 100644 index 000000000..4cbcd2f0c --- /dev/null +++ b/src/core/features/course/services/sync.ts @@ -0,0 +1,260 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreSyncBaseProvider } from '@classes/base-sync'; + +import { CoreSites } from '@services/sites'; +import { CoreApp } from '@services/app'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreCourseOffline } from './course-offline'; +import { CoreCourse } from './course'; +import { CoreCourseLogHelper } from './log-helper'; +import { CoreWSExternalWarning } from '@services/ws'; +import { CoreCourseManualCompletionDBRecord } from './database/course'; +import { CoreNetworkError } from '@classes/errors/network-error'; +import { makeSingleton, Translate } from '@singletons'; +import { CoreEvents, CoreEventSiteData } from '@singletons/events'; + +/** + * Service to sync course offline data. This only syncs the offline data of the course itself, not the offline data of + * the activities in the course. + */ +@Injectable({ providedIn: 'root' }) +export class CoreCourseSyncProvider extends CoreSyncBaseProvider { + + static readonly AUTO_SYNCED = 'core_course_autom_synced'; + + constructor() { + super('CoreCourseSyncProvider'); + } + + /** + * Try to synchronize all the courses in a certain site or in all sites. + * + * @param siteId Site ID to sync. If not defined, sync all sites. + * @param force Wether the execution is forced (manual sync). + * @return Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllCourses(siteId?: string, force?: boolean): Promise { + return this.syncOnSites('courses', this.syncAllCoursesFunc.bind(this, siteId, force), siteId); + } + + /** + * Sync all courses on a site. + * + * @param siteId Site ID to sync. + * @param force Wether the execution is forced (manual sync). + * @return Promise resolved if sync is successful, rejected if sync fails. + */ + protected async syncAllCoursesFunc(siteId: string, force: boolean): Promise { + await Promise.all([ + CoreCourseLogHelper.instance.syncSite(siteId), + this.syncCoursesCompletion(siteId, force), + ]); + } + + /** + * Sync courses offline completion. + * + * @param siteId Site ID to sync. + * @param force Wether the execution is forced (manual sync). + * @return Promise resolved if sync is successful, rejected if sync fails. + */ + protected async syncCoursesCompletion(siteId: string, force: boolean): Promise { + const completions = await CoreCourseOffline.instance.getAllManualCompletions(siteId); + + // Sync all courses. + await Promise.all(completions.map(async (completion) => { + const result = await (force ? this.syncCourse(completion.courseid, siteId) : + this.syncCourseIfNeeded(completion.courseid, siteId)); + + if (!result || !result.updated) { + return; + } + + // Sync successful, send event. + CoreEvents.trigger(CoreCourseSyncProvider.AUTO_SYNCED, { + courseId: completion.courseid, + warnings: result.warnings, + }, siteId); + })); + } + + /** + * Sync a course if it's needed. + * + * @param courseId Course ID to be synced. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the course is synced or it doesn't need to be synced. + */ + syncCourseIfNeeded(courseId: number, siteId?: string): Promise { + // Usually we call isSyncNeeded to check if a certain time has passed. + // However, since we barely send data for now just sync the course. + return this.syncCourse(courseId, siteId); + } + + /** + * Synchronize a course. + * + * @param courseId Course ID to be synced. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + async syncCourse(courseId: number, siteId?: string): Promise { + siteId = siteId || CoreSites.instance.getCurrentSiteId(); + + if (this.isSyncing(courseId, siteId)) { + // There's already a sync ongoing for this discussion, return the promise. + return this.getOngoingSync(courseId, siteId)!; + } + + this.logger.debug(`Try to sync course '${courseId}'`); + + return this.addOngoingSync(courseId, this.syncCourseCompletion(courseId, siteId), siteId); + } + + /** + * Sync course offline completion. + * + * @param courseId Course ID to be synced. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + protected async syncCourseCompletion(courseId: number, siteId?: string): Promise { + const result: CoreCourseSyncResult = { + warnings: [], + updated: false, + }; + + // Get offline responses to be sent. + const completions = await CoreUtils.instance.ignoreErrors( + CoreCourseOffline.instance.getCourseManualCompletions(courseId, siteId), + [], + ); + + + if (!completions || !completions.length) { + // Nothing to sync, set sync time. + await this.setSyncTime(String(courseId), siteId); + + // All done, return the data. + return result; + } + + if (!CoreApp.instance.isOnline()) { + // Cannot sync in offline. + throw new CoreNetworkError(); + } + + // Get the current completion status to check if any completion was modified in web. + // This can be retrieved on core_course_get_contents since 3.6 but this is an easy way to get them. + const onlineCompletions = await CoreCourse.instance.getActivitiesCompletionStatus( + courseId, + siteId, + undefined, + false, + true, + false, + ); + + // Send all the completions. + await Promise.all(completions.map(async (entry) => { + const onlineComp = onlineCompletions[entry.cmid]; + + // Check if the completion was modified in online. If so, discard it. + if (onlineComp && onlineComp.timecompleted * 1000 > entry.timecompleted) { + await CoreCourseOffline.instance.deleteManualCompletion(entry.cmid, siteId); + + // Completion deleted, add a warning if the completion status doesn't match. + if (onlineComp.state != entry.completed) { + result.warnings.push(Translate.instance.instant('core.course.warningofflinemanualcompletiondeleted', { + name: entry.coursename || courseId, + error: Translate.instance.instant('core.course.warningmanualcompletionmodified'), + })); + } + + return; + } + + try { + await CoreCourse.instance.markCompletedManuallyOnline(entry.cmid, !!entry.completed, siteId); + + result.updated = true; + + await CoreCourseOffline.instance.deleteManualCompletion(entry.cmid, siteId); + } catch (error) { + if (!CoreUtils.instance.isWebServiceError(error)) { + // Couldn't connect to server, reject. + throw error; + } + + // The WebService has thrown an error, this means that the completion cannot be submitted. Delete it. + result.updated = true; + + await CoreCourseOffline.instance.deleteManualCompletion(entry.cmid, siteId); + + // Completion deleted, add a warning. + result.warnings.push(Translate.instance.instant('core.course.warningofflinemanualcompletiondeleted', { + name: entry.coursename || courseId, + error: CoreTextUtils.instance.getErrorMessageFromError(error), + })); + } + })); + + if (result.updated) { + try { + // Update data. + await CoreCourse.instance.invalidateSections(courseId, siteId); + + const currentSite = CoreSites.instance.getCurrentSite(); + + if (currentSite?.isVersionGreaterEqualThan('3.6')) { + await CoreCourse.instance.getSections(courseId, false, true, undefined, siteId); + } else { + await CoreCourse.instance.getActivitiesCompletionStatus(courseId, siteId); + } + } catch { + // Ignore errors. + } + } + + // Sync finished, set sync time. + await this.setSyncTime(String(courseId), siteId); + + // All done, return the data. + return result; + } + +} + +export class CoreCourseSync extends makeSingleton(CoreCourseSyncProvider) {} + +/** + * Result of course sync. + */ +export type CoreCourseSyncResult = { + updated: boolean; + warnings: CoreWSExternalWarning[]; +}; + +/** + * Data passed to AUTO_SYNCED event. + */ +export type CoreCourseAutoSyncData = CoreEventSiteData & { + courseId: number; + warnings: CoreWSExternalWarning[]; +}; diff --git a/src/core/services/file-helper.ts b/src/core/services/file-helper.ts index 1623b80ac..11a29b0e1 100644 --- a/src/core/services/file-helper.ts +++ b/src/core/services/file-helper.ts @@ -26,6 +26,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { CoreError } from '@classes/errors/error'; import { makeSingleton, Translate } from '@singletons'; +import { CoreNetworkError } from '@classes/errors/network-error'; /** * Provider to provide some helper functions regarding files and packages. @@ -152,7 +153,7 @@ export class CoreFileHelperProvider { } else { if (!isOnline && !this.isStateDownloaded(state)) { // Not downloaded and user is offline, reject. - throw new CoreError(Translate.instance.instant('core.networkerrormsg')); + throw new CoreNetworkError(); } if (onProgress) { diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index bac5b6d49..b28c44c5e 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -49,6 +49,7 @@ import { SchemaVersionsDBEntry, } from '@services/database/sites'; import { CoreArray } from '../singletons/array'; +import { CoreNetworkError } from '@classes/errors/network-error'; export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); @@ -136,7 +137,7 @@ export class CoreSitesProvider { if (!CoreUrlUtils.instance.isHttpURL(siteUrl)) { throw new CoreError(Translate.instance.instant('core.login.invalidsite')); } else if (!CoreApp.instance.isOnline()) { - throw new CoreError(Translate.instance.instant('core.networkerrormsg')); + throw new CoreNetworkError(); } try { @@ -359,7 +360,7 @@ export class CoreSitesProvider { retry?: boolean, ): Promise { if (!CoreApp.instance.isOnline()) { - throw new CoreError(Translate.instance.instant('core.networkerrormsg')); + throw new CoreNetworkError(); } if (!service) { diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index e3a286976..490cee48f 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -35,6 +35,7 @@ import { CoreSilentError } from '@classes/errors/silenterror'; import { makeSingleton, Translate, AlertController, LoadingController, ToastController } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreFileSizeSum } from '@services/plugin-file-delegate'; +import { CoreNetworkError } from '@classes/errors/network-error'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -642,11 +643,13 @@ export class CoreDomUtilsProvider { * Given a message, it deduce if it's a network error. * * @param message Message text. + * @param error Error object. * @return True if the message error is a network error, false otherwise. */ - protected isNetworkError(message: string): boolean { + protected isNetworkError(message: string, error?: CoreError | CoreTextErrorObject | string): boolean { return message == Translate.instance.instant('core.networkerrormsg') || - message == Translate.instance.instant('core.fileuploader.errormustbeonlinetoupload'); + message == Translate.instance.instant('core.fileuploader.errormustbeonlinetoupload') || + error instanceof CoreNetworkError; } /** @@ -1365,7 +1368,7 @@ export class CoreDomUtilsProvider { buttons: [Translate.instance.instant('core.ok')], }; - if (this.isNetworkError(message)) { + if (this.isNetworkError(message, error)) { alertOptions.cssClass = 'core-alert-network-error'; } else { alertOptions.header = Translate.instance.instant('core.error'); diff --git a/src/core/services/ws.ts b/src/core/services/ws.ts index a290219cc..33b285cc8 100644 --- a/src/core/services/ws.ts +++ b/src/core/services/ws.ts @@ -36,6 +36,7 @@ import { CoreLogger } from '@singletons/logger'; import { CoreWSError } from '@classes/errors/wserror'; import { CoreAjaxError } from '@classes/errors/ajaxerror'; import { CoreAjaxWSError } from '@classes/errors/ajaxwserror'; +import { CoreNetworkError } from '@classes/errors/network-error'; /** * This service allows performing WS calls and download/upload files. @@ -107,7 +108,7 @@ export class CoreWSProvider { if (!preSets) { throw new CoreError(Translate.instance.instant('core.unexpectederror')); } else if (!CoreApp.instance.isOnline()) { - throw new CoreError(Translate.instance.instant('core.networkerrormsg')); + throw new CoreNetworkError(); } preSets.typeExpected = preSets.typeExpected || 'object'; @@ -249,7 +250,7 @@ export class CoreWSProvider { this.logger.debug('Downloading file', url, path, addExtension); if (!CoreApp.instance.isOnline()) { - throw new CoreError(Translate.instance.instant('core.networkerrormsg')); + throw new CoreNetworkError(); } // Use a tmp path to download the file and then move it to final location. @@ -741,7 +742,7 @@ export class CoreWSProvider { if (!preSets) { throw new CoreError(Translate.instance.instant('core.unexpectederror')); } else if (!CoreApp.instance.isOnline()) { - throw new CoreError(Translate.instance.instant('core.networkerrormsg')); + throw new CoreNetworkError(); } preSets.typeExpected = preSets.typeExpected || 'object'; @@ -825,7 +826,7 @@ export class CoreWSProvider { } if (!CoreApp.instance.isOnline()) { - throw new CoreError(Translate.instance.instant('core.networkerrormsg')); + throw new CoreNetworkError(); } const uploadUrl = preSets.siteUrl + '/webservice/upload.php';