diff --git a/src/addons/block/recentlyaccesseditems/services/recentlyaccesseditems.ts b/src/addons/block/recentlyaccesseditems/services/recentlyaccesseditems.ts index b9c790b5c..04f67c3e6 100644 --- a/src/addons/block/recentlyaccesseditems/services/recentlyaccesseditems.ts +++ b/src/addons/block/recentlyaccesseditems/services/recentlyaccesseditems.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreCourse } from '@features/course/services/course'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; import { makeSingleton } from '@singletons'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; diff --git a/src/addons/block/starredcourses/services/starredcourses.ts b/src/addons/block/starredcourses/services/starredcourses.ts index 380da1bec..27617ca75 100644 --- a/src/addons/block/starredcourses/services/starredcourses.ts +++ b/src/addons/block/starredcourses/services/starredcourses.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; import { makeSingleton } from '@singletons'; const ROOT_CACHE_KEY = 'AddonBlockStarredCourses:'; diff --git a/src/addons/block/timeline/services/timeline.ts b/src/addons/block/timeline/services/timeline.ts index a18750e46..5cb784b0f 100644 --- a/src/addons/block/timeline/services/timeline.ts +++ b/src/addons/block/timeline/services/timeline.ts @@ -24,7 +24,7 @@ import { } from '@addons/calendar/services/calendar'; import moment from 'moment-timezone'; import { makeSingleton } from '@singletons'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; // Cache key was maintained from block myoverview when blocks were splitted. const ROOT_CACHE_KEY = 'myoverview:'; diff --git a/src/addons/blog/services/blog.ts b/src/addons/blog/services/blog.ts index 65b680911..0732b9002 100644 --- a/src/addons/blog/services/blog.ts +++ b/src/addons/blog/services/blog.ts @@ -13,7 +13,8 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; +import { CoreSite } from '@classes/sites/site'; import { CoreTagItem } from '@features/tag/services/tag'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index e9668e221..552e30e81 100644 --- a/src/addons/calendar/services/calendar.ts +++ b/src/addons/calendar/services/calendar.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreNetwork } from '@services/network'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; @@ -50,6 +50,7 @@ import { } from '@features/reminders/services/reminders'; import { CoreReminderDBRecord } from '@features/reminders/services/database/reminders'; import { CoreEvents } from '@singletons/events'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaCalendar:'; diff --git a/src/addons/competency/services/competency.ts b/src/addons/competency/services/competency.ts index 629182998..d064722f6 100644 --- a/src/addons/competency/services/competency.ts +++ b/src/addons/competency/services/competency.ts @@ -13,7 +13,8 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCommentsArea } from '@features/comments/services/comments'; import { CoreCourseSummary, CoreCourseModuleSummary } from '@features/course/services/course'; import { CoreUserSummary } from '@features/user/services/user'; diff --git a/src/addons/coursecompletion/services/coursecompletion.ts b/src/addons/coursecompletion/services/coursecompletion.ts index 1d173941b..3067060e3 100644 --- a/src/addons/coursecompletion/services/coursecompletion.ts +++ b/src/addons/coursecompletion/services/coursecompletion.ts @@ -17,12 +17,13 @@ import { CoreLogger } from '@singletons/logger'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { CoreCourses } from '@features/courses/services/courses'; -import { CoreSite, CoreSiteWSPreSets, WSObservable } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton } from '@singletons'; import { CoreError } from '@classes/errors/error'; import { asyncObservable, firstValueFrom } from '@/core/utils/rxjs'; import { map } from 'rxjs/operators'; +import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaCourseCompletion:'; diff --git a/src/addons/enrol/guest/services/guest.ts b/src/addons/enrol/guest/services/guest.ts index fe37cbfe4..a9249f6f6 100644 --- a/src/addons/enrol/guest/services/guest.ts +++ b/src/addons/enrol/guest/services/guest.ts @@ -13,7 +13,8 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreSiteWSPreSets, CoreSite } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; +import { CoreSite } from '@classes/sites/site'; import { CoreEnrolEnrolmentInfo } from '@features/enrol/services/enrol'; import { CoreSites } from '@services/sites'; import { CoreWSExternalWarning } from '@services/ws'; diff --git a/src/addons/enrol/self/services/self.ts b/src/addons/enrol/self/services/self.ts index b20e32dd0..38de56740 100644 --- a/src/addons/enrol/self/services/self.ts +++ b/src/addons/enrol/self/services/self.ts @@ -14,7 +14,8 @@ import { Injectable } from '@angular/core'; import { CoreWSError } from '@classes/errors/wserror'; -import { CoreSiteWSPreSets, CoreSite } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCoursesProvider } from '@features/courses/services/courses'; import { CoreSites } from '@services/sites'; import { CoreStatusWithWarningsWSResponse } from '@services/ws'; diff --git a/src/addons/messageoutput/airnotifier/services/airnotifier.ts b/src/addons/messageoutput/airnotifier/services/airnotifier.ts index 9ed230627..6b5ace8ea 100644 --- a/src/addons/messageoutput/airnotifier/services/airnotifier.ts +++ b/src/addons/messageoutput/airnotifier/services/airnotifier.ts @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreConstants } from '@/core/constants'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreError } from '@classes/errors/error'; import { CoreWSError } from '@classes/errors/wserror'; import { makeSingleton, Translate } from '@singletons'; @@ -25,6 +25,7 @@ import { CoreEvents, CoreEventSiteData } from '@singletons/events'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { CorePath } from '@singletons/path'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaMessageOutputAirnotifier:'; diff --git a/src/addons/messages/services/messages-sync.ts b/src/addons/messages/services/messages-sync.ts index 954ce6693..879453a19 100644 --- a/src/addons/messages/services/messages-sync.ts +++ b/src/addons/messages/services/messages-sync.ts @@ -31,7 +31,7 @@ import { CoreConstants } from '@/core/constants'; import { CoreUser } from '@features/user/services/user'; import { CoreError } from '@classes/errors/error'; import { CoreTextErrorObject, CoreTextUtils } from '@services/utils/text'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; /** * Service to sync messages. diff --git a/src/addons/messages/services/messages.ts b/src/addons/messages/services/messages.ts index bfa00594c..31c1ab770 100644 --- a/src/addons/messages/services/messages.ts +++ b/src/addons/messages/services/messages.ts @@ -26,13 +26,14 @@ import { import { CoreUtils } from '@services/utils/utils'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreEvents } from '@singletons/events'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreWSExternalWarning } from '@services/ws'; import { makeSingleton } from '@singletons'; import { CoreError } from '@classes/errors/error'; import { AddonMessagesSyncEvents, AddonMessagesSyncProvider } from './messages-sync'; import { CoreWSError } from '@classes/errors/wserror'; import { AddonNotificationsPreferencesNotificationProcessorState } from '@addons/notifications/services/notifications'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaMessages:'; diff --git a/src/addons/mod/assign/services/assign.ts b/src/addons/mod/assign/services/assign.ts index 4e410ab08..4f78429ca 100644 --- a/src/addons/mod/assign/services/assign.ts +++ b/src/addons/mod/assign/services/assign.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreInterceptor } from '@classes/interceptor'; import { CoreWSExternalWarning, CoreWSExternalFile, CoreWSFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -35,6 +35,7 @@ import { AddonModAssignAutoSyncData, AddonModAssignManualSyncData, AddonModAssig import { CoreFormFields } from '@singletons/form'; import { CoreFileHelper } from '@services/file-helper'; import { CoreIonicColorNames } from '@singletons/colors'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaModAssign:'; diff --git a/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts b/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts index cc3f75043..59ac2bd77 100644 --- a/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts +++ b/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts @@ -15,7 +15,8 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; import { CoreWSError } from '@classes/errors/wserror'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; diff --git a/src/addons/mod/book/services/book.ts b/src/addons/mod/book/services/book.ts index 93a4b2e89..011e111cb 100644 --- a/src/addons/mod/book/services/book.ts +++ b/src/addons/mod/book/services/book.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreTagItem } from '@features/tag/services/tag'; import { CoreWSExternalWarning, CoreWSExternalFile, CoreWS } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -26,6 +26,7 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreFile } from '@services/file'; import { CoreError } from '@classes/errors/error'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; /** * Constants to define how the chapters and subchapters of a book should be displayed in that table of contents. diff --git a/src/addons/mod/chat/services/chat.ts b/src/addons/mod/chat/services/chat.ts index 8cf6d0346..a2cc552bb 100644 --- a/src/addons/mod/chat/services/chat.ts +++ b/src/addons/mod/chat/services/chat.ts @@ -14,7 +14,8 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreUser } from '@features/user/services/user'; diff --git a/src/addons/mod/choice/services/choice.ts b/src/addons/mod/choice/services/choice.ts index 5e99237a6..a4e7b1a54 100644 --- a/src/addons/mod/choice/services/choice.ts +++ b/src/addons/mod/choice/services/choice.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; import { CoreWSError } from '@classes/errors/wserror'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreNetwork } from '@services/network'; @@ -26,6 +26,7 @@ import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWar import { makeSingleton, Translate } from '@singletons'; import { AddonModChoiceOffline } from './choice-offline'; import { AddonModChoiceAutoSyncData, AddonModChoiceSyncProvider } from './choice-sync'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaModChoice:'; diff --git a/src/addons/mod/data/services/data.ts b/src/addons/mod/data/services/data.ts index 8ba1573dd..7d7f5a606 100644 --- a/src/addons/mod/data/services/data.ts +++ b/src/addons/mod/data/services/data.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreRatingInfo } from '@features/rating/services/rating'; @@ -29,6 +29,7 @@ import { makeSingleton, Translate } from '@singletons'; import { AddonModDataFieldsDelegate } from './data-fields-delegate'; import { AddonModDataOffline } from './data-offline'; import { AddonModDataAutoSyncData, AddonModDataSyncProvider } from './data-sync'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaModData:'; diff --git a/src/addons/mod/feedback/services/feedback.ts b/src/addons/mod/feedback/services/feedback.ts index 77fb8c281..abd4cdbca 100644 --- a/src/addons/mod/feedback/services/feedback.ts +++ b/src/addons/mod/feedback/services/feedback.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreNetwork } from '@services/network'; @@ -25,6 +25,7 @@ import { CoreWSExternalFile, CoreWSExternalWarning, CoreWSStoredFile } from '@se import { makeSingleton, Translate } from '@singletons'; import { AddonModFeedbackOffline } from './feedback-offline'; import { AddonModFeedbackAutoSyncData, AddonModFeedbackSyncProvider } from './feedback-sync'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'AddonModFeedback:'; diff --git a/src/addons/mod/folder/services/folder.ts b/src/addons/mod/folder/services/folder.ts index 8ec75b0c0..cc4d02f8d 100644 --- a/src/addons/mod/folder/services/folder.ts +++ b/src/addons/mod/folder/services/folder.ts @@ -14,7 +14,8 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourse } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; diff --git a/src/addons/mod/forum/services/forum.ts b/src/addons/mod/forum/services/forum.ts index 4b8eb076c..33d6025d0 100644 --- a/src/addons/mod/forum/services/forum.ts +++ b/src/addons/mod/forum/services/forum.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreRatingInfo } from '@features/rating/services/rating'; @@ -29,6 +29,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning, CoreWSStoredFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonModForumOffline, AddonModForumOfflineDiscussion, AddonModForumReplyOptions } from './forum-offline'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaModForum:'; diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts index db4686056..968e11603 100644 --- a/src/addons/mod/glossary/services/glossary.ts +++ b/src/addons/mod/glossary/services/glossary.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; @@ -28,6 +28,7 @@ import { makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { AddonModGlossaryEntryDBRecord, ENTRIES_TABLE_NAME } from './database/glossary'; import { AddonModGlossaryOffline } from './glossary-offline'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; export const GLOSSARY_ENTRY_ADDED = 'addon_mod_glossary_entry_added'; export const GLOSSARY_ENTRY_UPDATED = 'addon_mod_glossary_entry_updated'; diff --git a/src/addons/mod/h5pactivity/services/h5pactivity.ts b/src/addons/mod/h5pactivity/services/h5pactivity.ts index d2df04a1b..0d87a14ab 100644 --- a/src/addons/mod/h5pactivity/services/h5pactivity.ts +++ b/src/addons/mod/h5pactivity/services/h5pactivity.ts @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreWSExternalWarning, CoreWSExternalFile, CoreWSFile } from '@services/ws'; import { CoreUtils } from '@services/utils/utils'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreH5P } from '@features/h5p/services/h5p'; import { CoreH5PDisplayOptions } from '@features/h5p/classes/core'; @@ -27,6 +27,7 @@ import { CoreWSError } from '@classes/errors/wserror'; import { CoreError } from '@classes/errors/error'; import { AddonModH5PActivityAutoSyncData, AddonModH5PActivitySyncProvider } from './h5pactivity-sync'; import { CoreTime } from '@singletons/time'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; export const MOD_H5PACTIVITY_STATE_ID = 'state'; diff --git a/src/addons/mod/imscp/services/imscp.ts b/src/addons/mod/imscp/services/imscp.ts index 570e67df5..121c82d4c 100644 --- a/src/addons/mod/imscp/services/imscp.ts +++ b/src/addons/mod/imscp/services/imscp.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourse, CoreCourseModuleContentFile } from '@features/course/services/course'; import { CoreCourseModuleData } from '@features/course/services/course-helper'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; @@ -26,6 +26,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { CorePath } from '@singletons/path'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaModImscp:'; diff --git a/src/addons/mod/label/services/label.ts b/src/addons/mod/label/services/label.ts index b0ac56f3a..e0a9c9cc0 100644 --- a/src/addons/mod/label/services/label.ts +++ b/src/addons/mod/label/services/label.ts @@ -14,7 +14,8 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; +import { CoreSite } from '@classes/sites/site'; import { CoreFilepool } from '@services/filepool'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; diff --git a/src/addons/mod/lesson/services/lesson.ts b/src/addons/mod/lesson/services/lesson.ts index 221e58a15..f60cb486d 100644 --- a/src/addons/mod/lesson/services/lesson.ts +++ b/src/addons/mod/lesson/services/lesson.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreGradesProvider } from '@features/grades/services/grades'; @@ -28,6 +28,7 @@ import { CoreEvents } from '@singletons/events'; import { AddonModLessonPasswordDBRecord, PASSWORD_TABLE_NAME } from './database/lesson'; import { AddonModLessonOffline, AddonModLessonPageAttemptRecord } from './lesson-offline'; import { AddonModLessonAutoSyncData, AddonModLessonSyncProvider } from './lesson-sync'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaModLesson:'; diff --git a/src/addons/mod/lti/services/lti.ts b/src/addons/mod/lti/services/lti.ts index 42f787dce..94b1f2bc2 100644 --- a/src/addons/mod/lti/services/lti.ts +++ b/src/addons/mod/lti/services/lti.ts @@ -15,7 +15,8 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreFile } from '@services/file'; import { CorePlatform } from '@services/platform'; diff --git a/src/addons/mod/page/services/page.ts b/src/addons/mod/page/services/page.ts index 347ba6d58..b88af2498 100644 --- a/src/addons/mod/page/services/page.ts +++ b/src/addons/mod/page/services/page.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreSitesCommonWSOptions, CoreSites } from '@services/sites'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { CoreFilepool } from '@services/filepool'; @@ -22,6 +22,7 @@ import { CoreCourse } from '@features/course/services/course'; import { CoreUtils } from '@services/utils/utils'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreError } from '@classes/errors/error'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaModPage:'; diff --git a/src/addons/mod/quiz/services/quiz.ts b/src/addons/mod/quiz/services/quiz.ts index c1a919d41..d424fd684 100644 --- a/src/addons/mod/quiz/services/quiz.ts +++ b/src/addons/mod/quiz/services/quiz.ts @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; import { CoreWSError } from '@classes/errors/wserror'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreGradesFormattedItem, CoreGradesHelper } from '@features/grades/services/grades-helper'; @@ -40,6 +40,7 @@ import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate'; import { AddonModQuizAttempt } from './quiz-helper'; import { AddonModQuizOffline, AddonModQuizQuestionsWithAnswers } from './quiz-offline'; import { AddonModQuizAutoSyncData, AddonModQuizSyncProvider } from './quiz-sync'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaModQuiz:'; diff --git a/src/addons/mod/resource/services/resource.ts b/src/addons/mod/resource/services/resource.ts index 32261a9dd..a250f8978 100644 --- a/src/addons/mod/resource/services/resource.ts +++ b/src/addons/mod/resource/services/resource.ts @@ -14,7 +14,8 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourse } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreFilepool } from '@services/filepool'; diff --git a/src/addons/mod/scorm/services/scorm.ts b/src/addons/mod/scorm/services/scorm.ts index d91c08700..f468d7e9f 100644 --- a/src/addons/mod/scorm/services/scorm.ts +++ b/src/addons/mod/scorm/services/scorm.ts @@ -15,7 +15,7 @@ import { CoreConstants } from '@/core/constants'; import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreFilepool } from '@services/filepool'; @@ -31,6 +31,7 @@ import { CoreEvents } from '@singletons/events'; import { CorePath } from '@singletons/path'; import { AddonModScormOffline } from './scorm-offline'; import { AddonModScormAutoSyncEventData, AddonModScormSyncProvider } from './scorm-sync'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; // Private constants. const VALID_STATUSES = ['notattempted', 'passed', 'completed', 'failed', 'incomplete', 'browsed', 'suspend']; diff --git a/src/addons/mod/survey/services/survey.ts b/src/addons/mod/survey/services/survey.ts index ba412c317..1a21a9acd 100644 --- a/src/addons/mod/survey/services/survey.ts +++ b/src/addons/mod/survey/services/survey.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreNetwork } from '@services/network'; @@ -24,6 +24,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonModSurveyOffline } from './survey-offline'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaModSurvey:'; diff --git a/src/addons/mod/url/services/url.ts b/src/addons/mod/url/services/url.ts index 170fdeb91..90d92f7b1 100644 --- a/src/addons/mod/url/services/url.ts +++ b/src/addons/mod/url/services/url.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { CoreConstants } from '@/core/constants'; @@ -23,6 +23,7 @@ import { CoreCourse } from '@features/course/services/course'; import { CoreUtils } from '@services/utils/utils'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreError } from '@classes/errors/error'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaModUrl:'; diff --git a/src/addons/mod/wiki/services/wiki.ts b/src/addons/mod/wiki/services/wiki.ts index 4876e7551..9dc3d8f8d 100644 --- a/src/addons/mod/wiki/services/wiki.ts +++ b/src/addons/mod/wiki/services/wiki.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreTagItem } from '@features/tag/services/tag'; @@ -28,6 +28,7 @@ import { CoreEvents } from '@singletons/events'; import { AddonModWikiPageDBRecord } from './database/wiki'; import { AddonModWikiOffline } from './wiki-offline'; import { AddonModWikiAutoSyncData, AddonModWikiManualSyncData, AddonModWikiSyncProvider } from './wiki-sync'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaModWiki:'; diff --git a/src/addons/mod/workshop/services/workshop.ts b/src/addons/mod/workshop/services/workshop.ts index 1f3ddd9dc..0fe9ba780 100644 --- a/src/addons/mod/workshop/services/workshop.ts +++ b/src/addons/mod/workshop/services/workshop.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreGradesMenuItem } from '@features/grades/services/grades-helper'; @@ -28,6 +28,7 @@ import { CoreFormFields } from '@singletons/form'; import { AddonModWorkshopOffline } from './workshop-offline'; import { AddonModWorkshopAutoSyncData, AddonModWorkshopSyncProvider } from './workshop-sync'; import { ADDON_MOD_WORKSHOP_COMPONENT } from '@addons/mod/workshop/constants'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaModWorkshop:'; diff --git a/src/addons/notes/services/notes.ts b/src/addons/notes/services/notes.ts index e9f10dd4c..37f19187f 100644 --- a/src/addons/notes/services/notes.ts +++ b/src/addons/notes/services/notes.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreWSError } from '@classes/errors/wserror'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreUser } from '@features/user/services/user'; import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; @@ -22,6 +22,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonNotesOffline } from './notes-offline'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmaNotes:'; diff --git a/src/addons/notifications/services/notifications.ts b/src/addons/notifications/services/notifications.ts index fced63e5d..441da75f0 100644 --- a/src/addons/notifications/services/notifications.ts +++ b/src/addons/notifications/services/notifications.ts @@ -19,11 +19,12 @@ import { CoreWSExternalWarning } from '@services/ws'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUser, USER_NOREPLY_USER } from '@features/user/services/user'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreLogger } from '@singletons/logger'; import { Translate, makeSingleton } from '@singletons'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { AddonNotificationsPushNotification } from './handlers/push-click'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; declare module '@singletons/events' { diff --git a/src/core/classes/page-load-watcher.ts b/src/core/classes/page-load-watcher.ts index 2921ec65e..2ad6ec0de 100644 --- a/src/core/classes/page-load-watcher.ts +++ b/src/core/classes/page-load-watcher.ts @@ -18,7 +18,7 @@ import { Subscription } from 'rxjs'; import { AsyncDirective } from './async-directive'; import { PageLoadsManager } from './page-loads-manager'; import { CorePromisedValue } from './promised-value'; -import { WSObservable } from './sites/site'; +import { WSObservable } from './sites/candidate-site'; /** * Class to watch requests from a page load (including requests from page sub-components). diff --git a/src/core/classes/sites/candidate-site.ts b/src/core/classes/sites/candidate-site.ts new file mode 100644 index 000000000..d8da5ba01 --- /dev/null +++ b/src/core/classes/sites/candidate-site.ts @@ -0,0 +1,1765 @@ +// (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 { CoreApp } from '@services/app'; +import { CoreNetwork } from '@services/network'; +import { CoreEvents } from '@singletons/events'; +import { + CoreWS, + CoreWSPreSets, + CoreWSPreSetsSplitRequest, + CoreWSTypeExpected, +} from '@services/ws'; +import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreConstants } from '@/core/constants'; +import { CoreError } from '@classes/errors/error'; +import { CoreWSError } from '@classes/errors/wserror'; +import { CoreLogger } from '@singletons/logger'; +import { Translate } from '@singletons'; +import { CoreLang, CoreLangFormat } from '@services/lang'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreSilentError } from '../errors/silenterror'; +import { CorePromisedValue } from '@classes/promised-value'; +import { Observable, ObservableInput, ObservedValueOf, OperatorFunction, Subject } from 'rxjs'; +import { finalize, map, mergeMap } from 'rxjs/operators'; +import { firstValueFrom } from '../../utils/rxjs'; +import { CoreSiteError } from '@classes/errors/siteerror'; +import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config'; +import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse, CoreUnauthenticatedSite } from './unauthenticated-site'; +import { Md5 } from 'ts-md5'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreSiteWSCacheRecord } from '@services/database/sites'; +import { CoreErrorLogs } from '@singletons/error-logs'; + +/** + * Class that represents a site (combination of site + user) where the user has authenticated but the site hasn't been validated + * yet, it might be a site not supported by the app. + */ +export class CoreCandidateSite extends CoreUnauthenticatedSite { + + static readonly REQUEST_QUEUE_FORCE_WS = false; // Use "tool_mobile_call_external_functions" even for calling a single function. + + // Constants for cache update frequency. + static readonly FREQUENCY_USUALLY = 0; + static readonly FREQUENCY_OFTEN = 1; + static readonly FREQUENCY_SOMETIMES = 2; + static readonly FREQUENCY_RARELY = 3; + + static readonly MINIMUM_MOODLE_VERSION = '3.5'; + + // Versions of Moodle releases. + static readonly MOODLE_RELEASES = { + '3.5': 2018051700, + '3.6': 2018120300, + '3.7': 2019052000, + '3.8': 2019111800, + '3.9': 2020061500, + '3.10': 2020110900, + '3.11': 2021051700, + '4.0': 2022041900, + '4.1': 2022112800, + '4.2': 2023042400, + '4.3': 2023100900, + }; + + // Possible cache update frequencies. + protected static readonly UPDATE_FREQUENCIES = [ + CoreConstants.CONFIG.cache_update_frequency_usually || 420000, + CoreConstants.CONFIG.cache_update_frequency_often || 1200000, + CoreConstants.CONFIG.cache_update_frequency_sometimes || 3600000, + CoreConstants.CONFIG.cache_update_frequency_rarely || 43200000, + ]; + + // WS that we allow to call even if the site is logged out. + protected static readonly ALLOWED_LOGGEDOUT_WS = [ + 'core_user_remove_user_device', + ]; + + id?: string | undefined; // For candidate sites it will always be undefined. + token: string; + privateToken?: string; + infos?: CoreSiteInfo; + + protected logger: CoreLogger; + protected cleanUnicode = false; + protected offlineDisabled = false; + private memoryCache: Record = {}; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected ongoingRequests: Record | undefined>> = {}; + protected requestQueue: RequestQueueItem[] = []; + protected requestQueueTimeout: number | null = null; + + /** + * Create a site. + * + * @param siteUrl Site URL. + * @param token Site's WS token. + * @param privateToken Private token. + */ + constructor(siteUrl: string, token: string, privateToken?: string) { + super(siteUrl); + + this.token = token; + this.privateToken = privateToken; + this.logger = CoreLogger.getInstance('CoreCandidateSite'); + } + + /** + * Get site token. + * + * @returns Site token. + */ + getToken(): string { + return this.token; + } + + /** + * @inheritdoc + */ + getInfo(): CoreSiteInfo | undefined { + return this.infos; + } + + /** + * Get site private token. + * + * @returns Site private token. + */ + getPrivateToken(): string | undefined { + return this.privateToken; + } + + /** + * Get site user's ID. + * + * @returns User's ID. + */ + getUserId(): number { + if (!this.infos) { + // Shouldn't happen for authenticated sites. + throw new CoreError('Site info could not be fetched.'); + } + + return this.infos.userid; + } + + /** + * Get site Course ID for frontpage course. If not declared it will return 1 as default. + * + * @returns Site Home ID. + */ + getSiteHomeId(): number { + return this.infos?.siteid || 1; + } + + /** + * Set site token. + * + * @param token New token. + */ + setToken(token: string): void { + this.token = token; + } + + /** + * Set site private token. + * + * @param privateToken New private token. + */ + setPrivateToken(privateToken: string): void { + this.privateToken = privateToken; + } + + /** + * Check if user logged out from the site and needs to authenticate again. + * + * @returns Whether is logged out. + */ + isLoggedOut(): boolean { + return false; + } + + /** + * Set site info. + * + * @param infos New info. + */ + setInfo(infos?: CoreSiteInfo): void { + this.infos = infos; + + // Index function by name to speed up wsAvailable method. + if (infos?.functions) { + infos.functionsByName = CoreUtils.arrayToObject(infos.functions, 'name'); + } + } + + /** + * Check if current user is Admin. + * Works properly since v3.8. See more in: {@link} https://tracker.moodle.org/browse/MDL-65550 + * + * @returns Whether the user is Admin. + */ + isAdmin(): boolean { + return this.getInfo()?.userissiteadmin ?? false; + } + + /** + * Can the user access their private files? + * + * @returns Whether can access my files. + */ + canAccessMyFiles(): boolean { + const info = this.getInfo(); + + return !!(info && (info.usercanmanageownfiles === undefined || info.usercanmanageownfiles)); + } + + /** + * Can the user download files? + * + * @returns Whether can download files. + */ + canDownloadFiles(): boolean { + const info = this.getInfo(); + + return !!info?.downloadfiles && info?.downloadfiles > 0; + } + + /** + * Can the user use an advanced feature? + * + * @param featureName The name of the feature. + * @param whenUndefined The value to return when the parameter is undefined. + * @returns Whether can use advanced feature. + */ + canUseAdvancedFeature(featureName: string, whenUndefined: boolean = true): boolean { + const info = this.getInfo(); + + if (info?.advancedfeatures === undefined) { + return whenUndefined; + } + + const feature = info.advancedfeatures.find((item) => item.name === featureName); + + if (!feature) { + return whenUndefined; + } + + return feature.value !== 0; + } + + /** + * Can the user upload files? + * + * @returns Whether can upload files. + */ + canUploadFiles(): boolean { + const info = this.getInfo(); + + return !!info?.uploadfiles && info?.uploadfiles > 0; + } + + /** + * Fetch site info from the Moodle site. + * + * @returns A promise to be resolved when the site info is retrieved. + */ + fetchSiteInfo(): Promise { + // The get_site_info WS call won't be cached. + const preSets = { + getFromCache: false, + saveToCache: false, + skipQueue: true, + }; + + // Reset clean Unicode to check if it's supported again. + this.cleanUnicode = false; + + return this.read('core_webservice_get_site_info', {}, preSets); + } + + /** + * Read some data from the Moodle site using WS. Requests are cached by default. + * + * @param method WS method to use. + * @param data Data to send to the WS. + * @param preSets Extra options. + * @returns Promise resolved with the response, rejected with CoreWSError if it fails. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + read(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise { + return firstValueFrom(this.readObservable(method, data, preSets)); + } + + /** + * Read some data from the Moodle site using WS. Requests are cached by default. + * + * @param method WS method to use. + * @param data Data to send to the WS. + * @param preSets Extra options. + * @returns Observable returning the WS data. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readObservable(method: string, data: any, preSets?: CoreSiteWSPreSets): WSObservable { + preSets = preSets || {}; + preSets.getFromCache = preSets.getFromCache ?? true; + preSets.saveToCache = preSets.saveToCache ?? true; + preSets.reusePending = preSets.reusePending ?? true; + + return this.requestObservable(method, data, preSets); + } + + /** + * Sends some data to the Moodle site using WS. Requests are NOT cached by default. + * + * @param method WS method to use. + * @param data Data to send to the WS. + * @param preSets Extra options. + * @returns Promise resolved with the response, rejected with CoreWSError if it fails. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + write(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise { + return firstValueFrom(this.writeObservable(method, data, preSets)); + } + + /** + * Sends some data to the Moodle site using WS. Requests are NOT cached by default. + * + * @param method WS method to use. + * @param data Data to send to the WS. + * @param preSets Extra options. + * @returns Observable returning the WS data. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + writeObservable(method: string, data: any, preSets?: CoreSiteWSPreSets): WSObservable { + preSets = preSets || {}; + preSets.getFromCache = preSets.getFromCache ?? false; + preSets.saveToCache = preSets.saveToCache ?? false; + preSets.emergencyCache = preSets.emergencyCache ?? false; + + return this.requestObservable(method, data, preSets); + } + + /** + * WS request to the site. + * + * @param method The WebService method to be called. + * @param data Arguments to pass to the method. + * @param preSets Extra options. + * @returns Promise resolved with the response, rejected with CoreWSError if it fails. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async request(method: string, data: any, preSets: CoreSiteWSPreSets): Promise { + return firstValueFrom(this.requestObservable(method, data, preSets)); + } + + /** + * WS request to the site. + * + * @param method The WebService method to be called. + * @param data Arguments to pass to the method. + * @param preSets Extra options. + * @returns Observable returning the WS data. + * @description + * + * Sends a webservice request to the site. This method will automatically add the + * required parameters and pass it on to the low level API in CoreWSProvider.call(). + * + * Caching is also implemented, when enabled this method will returned a cached version of the request if the + * data hasn't expired. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + requestObservable(method: string, data: any, preSets: CoreSiteWSPreSets): WSObservable { + if (this.isLoggedOut() && !CoreCandidateSite.ALLOWED_LOGGEDOUT_WS.includes(method)) { + // Site is logged out, it cannot call WebServices. + CoreEvents.trigger(CoreEvents.SESSION_EXPIRED, {}, this.id); + + // Use a silent error, the SESSION_EXPIRED event will display a message if needed. + throw new CoreSilentError(Translate.instant('core.lostconnection')); + } + + data = data || {}; + + if (!CoreNetwork.isOnline() && this.offlineDisabled) { + throw new CoreError(Translate.instant('core.errorofflinedisabled')); + } + + // Check if the method is available. + // We ignore this check when we do not have the site info, as the list of functions is not loaded yet. + if (this.getInfo() && !this.wsAvailable(method)) { + this.logger.error(`WS function '${method}' is not available.`); + + throw new CoreError(Translate.instant('core.wsfunctionnotavailable')); + } + + const wsPreSets: CoreWSPreSets = { + wsToken: this.token || '', + siteUrl: this.siteUrl, + cleanUnicode: this.cleanUnicode, + typeExpected: preSets.typeExpected, + responseExpected: preSets.responseExpected, + splitRequest: preSets.splitRequest, + }; + + if (wsPreSets.cleanUnicode && CoreTextUtils.hasUnicodeData(data)) { + // Data will be cleaned, notify the user. + CoreDomUtils.showToast('core.unicodenotsupported', true, ToastDuration.LONG); + } else { + // No need to clean data in this call. + wsPreSets.cleanUnicode = false; + } + + if (this.offlineDisabled) { + // Offline is disabled, don't use cache. + preSets.getFromCache = false; + preSets.saveToCache = false; + preSets.emergencyCache = false; + } + + // Enable text filtering by default. + data.moodlewssettingfilter = preSets.filter === false ? false : true; + data.moodlewssettingfileurl = preSets.rewriteurls === false ? false : true; + + // Convert arguments to strings before starting the cache process. + data = CoreWS.convertValuesToString(data, wsPreSets.cleanUnicode); + if (data == null) { + // Empty cleaned text found. + throw new CoreError(Translate.instant('core.unicodenotsupportedcleanerror')); + } + + const cacheId = this.getCacheId(method, data); + + // Check for an ongoing identical request. + const ongoingRequest = this.getOngoingRequest(cacheId, preSets); + if (ongoingRequest) { + return ongoingRequest; + } + + const observable = this.performRequest(method, data, preSets, wsPreSets).pipe( + // Return a clone of the original object, this may prevent errors if in the callback the object is modified. + map((data) => CoreUtils.clone(data)), + ); + + this.setOngoingRequest(cacheId, preSets, observable); + + return observable.pipe( + finalize(() => { + this.clearOngoingRequest(cacheId, preSets, observable); + }), + ); + } + + /** + * Get an ongoing request if there's one already. + * + * @param cacheId Cache ID. + * @param preSets Presets. + * @returns Ongoing request if it exists. + */ + protected getOngoingRequest(cacheId: string, preSets: CoreSiteWSPreSets): WSObservable | undefined { + if (preSets.updateInBackground) { + return this.ongoingRequests[cacheId]?.[OngoingRequestType.UPDATE_IN_BACKGROUND]; + } else if (preSets.getFromCache) { // Only reuse ongoing request when using cache. + return this.ongoingRequests[cacheId]?.[OngoingRequestType.STANDARD]; + } + } + + /** + * Store an ongoing request in memory. + * + * @param cacheId Cache ID. + * @param preSets Presets. + * @param request Request to store. + */ + protected setOngoingRequest(cacheId: string, preSets: CoreSiteWSPreSets, request: WSObservable): void { + this.ongoingRequests[cacheId] = this.ongoingRequests[cacheId] ?? {}; + + if (preSets.updateInBackground) { + this.ongoingRequests[cacheId][OngoingRequestType.UPDATE_IN_BACKGROUND] = request; + } else { + this.ongoingRequests[cacheId][OngoingRequestType.STANDARD] = request; + } + } + + /** + * Clear the ongoing request unless it has changed (e.g. a new request that ignores cache). + * + * @param cacheId Cache ID. + * @param preSets Presets. + * @param request Current request. + */ + protected clearOngoingRequest(cacheId: string, preSets: CoreSiteWSPreSets, request: WSObservable): void { + this.ongoingRequests[cacheId] = this.ongoingRequests[cacheId] ?? {}; + + if (preSets.updateInBackground) { + if (this.ongoingRequests[cacheId][OngoingRequestType.UPDATE_IN_BACKGROUND] === request) { + delete this.ongoingRequests[cacheId][OngoingRequestType.UPDATE_IN_BACKGROUND]; + } + } else { + if (this.ongoingRequests[cacheId][OngoingRequestType.STANDARD] === request) { + delete this.ongoingRequests[cacheId][OngoingRequestType.STANDARD]; + } + } + } + + /** + * Perform a request, getting the response either from cache or WebService. + * + * @param method The WebService method to be called. + * @param data Arguments to pass to the method. + * @param preSets Extra options related to the site. + * @param wsPreSets Extra options related to the WS call. + * @returns Observable returning the WS data. + */ + protected performRequest( + method: string, + data: unknown, + preSets: CoreSiteWSPreSets, + wsPreSets: CoreWSPreSets, + ): WSObservable { + const subject = new Subject(); + + const run = async () => { + try { + let response: T | WSCachedError; + let cachedData: WSCachedData | undefined; + + try { + cachedData = await this.getFromCache(method, data, preSets, false); + response = cachedData.response; + } catch { + // Not found or expired, call WS. + response = await this.getFromWS(method, data, preSets, wsPreSets); + } + + if ( + typeof response === 'object' && response !== null && + ( + ('exception' in response && response.exception !== undefined) || + ('errorcode' in response && response.errorcode !== undefined) + ) + ) { + subject.error(new CoreWSError(response)); + } else { + subject.next( response); + } + + if ( + preSets.updateInBackground && + !CoreConstants.CONFIG.disableCallWSInBackground && + cachedData && + !cachedData.expirationIgnored && + cachedData.expirationTime !== undefined && + Date.now() > cachedData.expirationTime + ) { + // Update the data in background. + setTimeout(async () => { + try { + preSets = { + ...preSets, + emergencyCache: false, + }; + + const newData = await this.getFromWS(method, data, preSets, wsPreSets); + + subject.next(newData); + } catch (error) { + // Ignore errors when updating in background. + this.logger.error('Error updating WS data in background', error); + } finally { + subject.complete(); + } + }); + } else { + // No need to update in background, complete the observable. + subject.complete(); + } + } catch (error) { + subject.error(error); + } + }; + + run(); + + return subject; + } + + /** + * Get a request response from WS, if it fails it might try to get it from emergency cache. + * + * @param method The WebService method to be called. + * @param data Arguments to pass to the method. + * @param preSets Extra options related to the site. + * @param wsPreSets Extra options related to the WS call. + * @returns Promise resolved with the response. + */ + protected async getFromWS( + method: string, + data: any, // eslint-disable-line @typescript-eslint/no-explicit-any + preSets: CoreSiteWSPreSets, + wsPreSets: CoreWSPreSets, + ): Promise { + if (preSets.forceOffline) { + // Don't call the WS, just fail. + throw new CoreError(Translate.instant('core.cannotconnect')); + } + + try { + const response = await this.callOrEnqueueWS(method, data, preSets, wsPreSets); + + if (preSets.saveToCache) { + this.saveToCache(method, data, response, preSets); + } + + return response; + } catch (error) { + let useSilentError = false; + + if (CoreUtils.isExpiredTokenError(error)) { + // Session expired, trigger event. + CoreEvents.trigger(CoreEvents.SESSION_EXPIRED, {}, this.id); + // Change error message. Try to get data from cache, the event will handle the error. + error.message = Translate.instant('core.lostconnection'); + useSilentError = true; // Use a silent error, the SESSION_EXPIRED event will display a message if needed. + } else if (error.errorcode === 'userdeleted' || error.errorcode === 'wsaccessuserdeleted') { + // User deleted, trigger event. + CoreEvents.trigger(CoreEvents.USER_DELETED, { params: data }, this.id); + error.message = Translate.instant('core.userdeleted'); + + throw new CoreWSError(error); + } else if (error.errorcode === 'wsaccessusersuspended') { + // User suspended, trigger event. + CoreEvents.trigger(CoreEvents.USER_SUSPENDED, { params: data }, this.id); + error.message = Translate.instant('core.usersuspended'); + + throw new CoreWSError(error); + } else if (error.errorcode === 'wsaccessusernologin') { + // User suspended, trigger event. + CoreEvents.trigger(CoreEvents.USER_NO_LOGIN, { params: data }, this.id); + error.message = Translate.instant('core.usernologin'); + + throw new CoreWSError(error); + } else if (error.errorcode === 'forcepasswordchangenotice') { + // Password Change Forced, trigger event. Try to get data from cache, the event will handle the error. + CoreEvents.trigger(CoreEvents.PASSWORD_CHANGE_FORCED, {}, this.id); + error.message = Translate.instant('core.forcepasswordchangenotice'); + useSilentError = true; // Use a silent error, the change password page already displays the appropiate info. + } else if (error.errorcode === 'usernotfullysetup') { + // User not fully setup, trigger event. Try to get data from cache, the event will handle the error. + CoreEvents.trigger(CoreEvents.USER_NOT_FULLY_SETUP, {}, this.id); + error.message = Translate.instant('core.usernotfullysetup'); + useSilentError = true; // Use a silent error, the complete profile page already displays the appropiate info. + } else if (error.errorcode === 'sitepolicynotagreed') { + // Site policy not agreed, trigger event. + CoreEvents.trigger(CoreEvents.SITE_POLICY_NOT_AGREED, {}, this.id); + error.message = Translate.instant('core.login.sitepolicynotagreederror'); + + throw new CoreWSError(error); + } else if (error.errorcode === 'dmlwriteexception' && CoreTextUtils.hasUnicodeData(data)) { + if (!this.cleanUnicode) { + // Try again cleaning unicode. + this.cleanUnicode = true; + + return this.request(method, data, preSets); + } + // This should not happen. + error.message = Translate.instant('core.unicodenotsupported'); + + throw new CoreWSError(error); + } else if (error.exception === 'required_capability_exception' || error.errorcode === 'nopermission' || + error.errorcode === 'notingroup') { + // Translate error messages with missing strings. + if (error.message === 'error/nopermission') { + error.message = Translate.instant('core.nopermissionerror'); + } else if (error.message === 'error/notingroup') { + error.message = Translate.instant('core.notingroup'); + } + + if (preSets.saveToCache) { + // Save the error instead of deleting the cache entry so the same content is displayed in offline. + this.saveToCache(method, data, error, preSets); + } + + throw new CoreWSError(error); + } else if (preSets.cacheErrors && preSets.cacheErrors.indexOf(error.errorcode) != -1) { + // Save the error instead of deleting the cache entry so the same content is displayed in offline. + this.saveToCache(method, data, error, preSets); + + throw new CoreWSError(error); + } else if (preSets.emergencyCache !== undefined && !preSets.emergencyCache) { + this.logger.debug(`WS call '${method}' failed. Emergency cache is forbidden, rejecting.`); + + throw new CoreWSError(error); + } + + if (preSets.deleteCacheIfWSError && CoreUtils.isWebServiceError(error)) { + // Delete the cache entry and return the entry. Don't block the user with the delete. + CoreUtils.ignoreErrors(this.deleteFromCache(method, data, preSets)); + + throw new CoreWSError(error); + } + + this.logger.debug(`WS call '${method}' failed. Trying to use the emergency cache.`); + preSets = { + ...preSets, + omitExpires: true, + getFromCache: true, + }; + + try { + const cachedData = await this.getFromCache(method, data, preSets, true); + + if ( + typeof cachedData.response === 'object' && cachedData.response !== null && + ( + ('exception' in cachedData.response && cachedData.response.exception !== undefined) || + ('errorcode' in cachedData.response && cachedData.response.errorcode !== undefined) + ) + ) { + throw new CoreWSError(cachedData.response); + } + + return cachedData.response; + } catch { + if (useSilentError) { + throw new CoreSilentError(error.message); + } + + throw new CoreWSError(error); + } + } + } + + /** + * Get a request response from WS. + * + * @param method The WebService method to be called. + * @param data Arguments to pass to the method. + * @param preSets Extra options related to the site. + * @param wsPreSets Extra options related to the WS call. + * @returns Promise resolved with the response. + */ + protected async callOrEnqueueWS( + method: string, + data: any, // eslint-disable-line @typescript-eslint/no-explicit-any + preSets: CoreSiteWSPreSets, + wsPreSets: CoreWSPreSets, + ): Promise { + // Call the WS. + const initialToken = this.token ?? ''; + + // Send the language to use. Do it after checking cache to prevent losing offline data when changing language. + // Moodle uses underscore instead of dash. + data = { + ...data, + moodlewssettinglang: CoreLang.formatLanguage(preSets.lang ?? await CoreLang.getCurrentLanguage(), CoreLangFormat.LMS), + }; + + try { + return await this.callOrEnqueueRequest(method, data, preSets, wsPreSets); + } catch (error) { + if (CoreUtils.isExpiredTokenError(error)) { + if (initialToken !== this.token) { + // Token has changed, retry with the new token. + wsPreSets.wsToken = this.token ?? ''; + + return await this.callOrEnqueueRequest(method, data, preSets, wsPreSets); + } else if (CoreApp.isSSOAuthenticationOngoing()) { + // There's an SSO authentication ongoing, wait for it to finish and try again. + await CoreApp.waitForSSOAuthentication(); + + return await this.callOrEnqueueRequest(method, data, preSets, wsPreSets); + } + } + + if (error?.errorcode === 'invalidparameter' && method === 'core_webservice_get_site_info') { + // Retry without passing the lang, this parameter isn't supported in 3.4 or older sites + // and we need this WS call to be able to determine if the site is supported or not. + delete data.moodlewssettinglang; + + return await this.callOrEnqueueRequest(method, data, preSets, wsPreSets); + } + + throw error; + } + } + + /** + * Adds a request to the queue or calls it immediately when not using the queue. + * + * @param method The WebService method to be called. + * @param data Arguments to pass to the method. + * @param preSets Extra options related to the site. + * @param wsPreSets Extra options related to the WS call. + * @returns Promise resolved with the response when the WS is called. + */ + protected callOrEnqueueRequest( + method: string, + data: any, // eslint-disable-line @typescript-eslint/no-explicit-any + preSets: CoreSiteWSPreSets, + wsPreSets: CoreWSPreSets, + ): Promise { + if (preSets.skipQueue || !this.wsAvailable('tool_mobile_call_external_functions')) { + return CoreWS.call(method, data, wsPreSets); + } + + const cacheId = this.getCacheId(method, data); + + // Check if there is an identical request waiting in the queue (read requests only by default). + if (preSets.reusePending) { + const request = this.requestQueue.find((request) => request.cacheId == cacheId); + if (request) { + return request.deferred; + } + } + + const request: RequestQueueItem = { + cacheId, + method, + data, + preSets, + wsPreSets, + deferred: new CorePromisedValue(), + }; + + return this.enqueueRequest(request); + } + + /** + * Adds a request to the queue. + * + * @param request The request to enqueue. + * @returns Promise resolved with the response when the WS is called. + */ + protected enqueueRequest(request: RequestQueueItem): Promise { + this.requestQueue.push(request); + + if (this.requestQueue.length >= CoreConstants.CONFIG.wsrequestqueuelimit) { + this.processRequestQueue(); + } else if (!this.requestQueueTimeout) { + this.requestQueueTimeout = window.setTimeout( + () => this.processRequestQueue(), + CoreConstants.CONFIG.wsrequestqueuedelay, + ); + } + + return request.deferred; + } + + /** + * Call the enqueued web service requests. + */ + protected async processRequestQueue(): Promise { + this.logger.debug(`Processing request queue (${this.requestQueue.length} requests)`); + + // Clear timeout if set. + if (this.requestQueueTimeout) { + clearTimeout(this.requestQueueTimeout); + this.requestQueueTimeout = null; + } + + // Extract all requests from the queue. + const requests = this.requestQueue; + this.requestQueue = []; + + if (requests.length == 1 && !CoreCandidateSite.REQUEST_QUEUE_FORCE_WS) { + // Only one request, do a regular web service call. + try { + const data = await CoreWS.call(requests[0].method, requests[0].data, requests[0].wsPreSets); + + requests[0].deferred.resolve(data); + } catch (error) { + requests[0].deferred.reject(error); + } + + return; + } + + let lang: string | undefined; + const requestsData: Record = { + requests: requests.map((request) => { + const args = {}; + const settings = {}; + + // Separate WS settings from function arguments. + Object.keys(request.data).forEach((key) => { + let value = request.data[key]; + const match = /^moodlews(setting.*)$/.exec(key); + if (match) { + if (match[1] == 'settingfilter' || match[1] == 'settingfileurl') { + // Undo special treatment of these settings in CoreWSProvider.convertValuesToString. + value = (value == 'true' ? '1' : '0'); + } else if (match[1] == 'settinglang') { + // Use the lang globally to avoid exceptions with languages not installed. + lang = value; + + return; + } + settings[match[1]] = value; + } else { + args[key] = value; + } + }); + + return { + function: request.method, + arguments: JSON.stringify(args), + ...settings, + }; + }), + }; + requestsData.moodlewssettinglang = lang; + + const wsPresets: CoreWSPreSets = { + siteUrl: this.siteUrl, + wsToken: this.token || '', + }; + + try { + const data = await CoreWS.call( + 'tool_mobile_call_external_functions', + requestsData, + wsPresets, + ); + + if (!data || !data.responses) { + throw new CoreSiteError({ + supportConfig: new CoreUserAuthenticatedSupportConfig(this), + message: Translate.instant('core.siteunavailablehelp', { site: this.siteUrl }), + errorcode: 'invalidresponse', + errorDetails: Translate.instant('core.errorinvalidresponse', { method: 'tool_mobile_call_external_functions' }), + }); + } + + requests.forEach((request, i) => { + const response = data.responses[i]; + + if (!response) { + // Request not executed, enqueue again. + this.enqueueRequest(request); + } else if (response.error) { + const rejectReason = CoreTextUtils.parseJSON(response.exception || '') as Error | undefined; + request.deferred.reject(rejectReason); + CoreErrorLogs.addErrorLog({ + method: request.method, + type: 'CoreSiteError', + message: response.exception ?? '', + time: new Date().getTime(), + data: request.data, + }); + } else { + let responseData = response.data ? CoreTextUtils.parseJSON(response.data) : {}; + // Match the behaviour of CoreWSProvider.call when no response is expected. + const responseExpected = wsPresets.responseExpected === undefined || wsPresets.responseExpected; + if (!responseExpected && (responseData == null || responseData === '')) { + responseData = {}; + } + request.deferred.resolve(responseData); + } + }); + } catch (error) { + // Error not specific to a single request, reject all promises. + requests.forEach((request) => { + CoreErrorLogs.addErrorLog({ + method: request.method, + type: 'CoreSiteError', + message: String(error) ?? '', + time: new Date().getTime(), + data: request.data, + }); + request.deferred.reject(error); + }); + } + } + + /** + * Check if a WS is available in this site. + * + * @param method WS name. + * @returns Whether the WS is available. + */ + wsAvailable(method: string): boolean { + return !!this.infos?.functionsByName?.[method]; + } + + /** + * Get cache ID. + * + * @param method The WebService method. + * @param data Arguments to pass to the method. + * @returns Cache ID. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected getCacheId(method: string, data: any): string { + return Md5.hashAsciiStr(method + ':' + CoreUtils.sortAndStringify(data)); + } + + /** + * Get a WS response from cache. + * + * @param method The WebService method to be called. + * @param data Arguments to pass to the method. + * @param preSets Extra options. + * @param emergency Whether it's an "emergency" cache call (WS call failed). + * @returns Cached data. + */ + protected async getFromCache( + method: string, + data: any, // eslint-disable-line @typescript-eslint/no-explicit-any + preSets: CoreSiteWSPreSets, + emergency?: boolean, + ): Promise> { + if (!preSets.getFromCache) { + throw new CoreError('Get from cache is disabled.'); + } + + const id = this.getCacheId(method, data); + let entry: CoreSiteWSCacheRecord | undefined; + + if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) { + const entries = await this.getCacheEntriesByKey(preSets.cacheKey ?? ''); + + if (!entries.length) { + // Cache key not found, get by params sent. + entry = await this.getCacheEntryById(id); + } else { + if (entries.length > 1) { + // More than one entry found. Search the one with same ID as this call. + entry = entries.find((entry) => entry.id == id); + } + + if (!entry) { + entry = entries[0]; + } + } + } else { + entry = await this.getCacheEntryById(id); + } + + if (entry === undefined) { + throw new CoreError('Cache entry not valid.'); + } + + const now = Date.now(); + let expirationTime: number | undefined; + + const forceCache = preSets.omitExpires || preSets.forceOffline || !CoreNetwork.isOnline(); + + if (!forceCache) { + expirationTime = entry.expirationTime + this.getExpirationDelay(preSets.updateFrequency); + + if (preSets.updateInBackground && !CoreConstants.CONFIG.disableCallWSInBackground) { + // Use a extended expiration time. + const extendedTime = entry.expirationTime + + (CoreConstants.CONFIG.callWSInBackgroundExpirationTime ?? CoreConstants.SECONDS_WEEK * 1000); + + if (now > extendedTime) { + this.logger.debug('Cached element found, but it is expired even for call WS in background.'); + + throw new CoreError('Cache entry is expired.'); + } + } else if (now > expirationTime) { + this.logger.debug('Cached element found, but it is expired'); + + throw new CoreError('Cache entry is expired.'); + } + } + + if (entry.data !== undefined) { + if (!expirationTime) { + this.logger.info(`Cached element found, id: ${id}. Expiration time ignored.`); + } else { + const expires = (expirationTime - now) / 1000; + this.logger.info(`Cached element found, id: ${id}. Expires in expires in ${expires} seconds`); + } + + return { + response: CoreTextUtils.parseJSON(entry.data, {}), + expirationIgnored: forceCache, + expirationTime, + }; + } + + throw new CoreError('Cache entry not valid.'); + } + + /** + * Get cache entry by ID. + * + * @param id Cache ID. + * @returns Cache entry. + */ + protected async getCacheEntryById(id: string): Promise { + if (!this.memoryCache[id]) { + throw new CoreError('Cache entry not found.'); + } + + return this.memoryCache[id]; + } + + /** + * Get cache entries by key. + * + * @param key Cache key. + * @returns Cache entries. + */ + protected async getCacheEntriesByKey(key: string): Promise { + return Object.values(this.memoryCache).filter(entry => entry.key === key); + } + + /** + * Save a WS response to cache. + * + * @param method The WebService method. + * @param data Arguments to pass to the method. + * @param response The WS response. + * @param preSets Extra options. + * @returns Promise resolved when the response is saved. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected async saveToCache(method: string, data: any, response: any, preSets: CoreSiteWSPreSets): Promise { + if (preSets.uniqueCacheKey) { + // Cache key must be unique, delete all entries with same cache key. + await CoreUtils.ignoreErrors(this.deleteFromCache(method, data, preSets, true)); + } + + // Since 3.7, the expiration time contains the time the entry is modified instead of the expiration time. + // We decided to reuse this field to prevent modifying the database table. + const id = this.getCacheId(method, data); + const entry: CoreSiteWSCacheRecord = { + id, + data: JSON.stringify(response), + expirationTime: Date.now(), + }; + + if (preSets.cacheKey) { + entry.key = preSets.cacheKey; + } + + if (preSets.component) { + entry.component = preSets.component; + if (preSets.componentId) { + entry.componentId = preSets.componentId; + } + } + + await this.storeCacheEntry(entry); + } + + /** + * Store a cache entry. + * + * @param entry Entry to store. + */ + protected async storeCacheEntry(entry: CoreSiteWSCacheRecord): Promise { + this.memoryCache[entry.id] = entry; + } + + /** + * Delete a WS cache entry or entries. + * + * @param method The WebService method to be called. + * @param data Arguments to pass to the method. + * @param preSets Extra options. + * @param allCacheKey True to delete all entries with the cache key, false to delete only by ID. + * @returns Promise resolved when the entries are deleted. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected async deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise { + if (allCacheKey) { + const entriesToDelete = await this.getCacheEntriesByKey(preSets.cacheKey ?? ''); + + entriesToDelete.forEach(entry => { + delete this.memoryCache[entry.id]; + }); + } else { + delete this.memoryCache[this.getCacheId(method, data)]; + } + } + + /** + * Invalidates all the cache entries. + * + * @returns Promise resolved when the cache entries are invalidated. + */ + async invalidateWsCache(): Promise { + try { + for (const id in this.memoryCache) { + this.memoryCache[id].expirationTime = 0; + } + } finally { + CoreEvents.trigger(CoreEvents.WS_CACHE_INVALIDATED, {}, this.id); + } + } + + /** + * Invalidates all the cache entries with a certain key. + * + * @param key Key to search. + * @returns Promise resolved when the cache entries are invalidated. + */ + async invalidateWsCacheForKey(key: string): Promise { + if (!key) { + return; + } + + this.logger.debug('Invalidate cache for key: ' + key); + + const entries = await this.getCacheEntriesByKey(key); + entries.forEach(entry => { + entry.expirationTime = 0; + }); + } + + /** + * Invalidates all the cache entries in an array of keys. + * + * @param keys Keys to search. + * @returns Promise resolved when the cache entries are invalidated. + */ + async invalidateMultipleWsCacheForKey(keys: string[]): Promise { + if (!keys || !keys.length) { + return; + } + + this.logger.debug('Invalidating multiple cache keys'); + await Promise.all(keys.map((key) => this.invalidateWsCacheForKey(key))); + } + + /** + * Invalidates all the cache entries whose key starts with a certain value. + * + * @param key Key to search. + * @returns Promise resolved when the cache entries are invalidated. + */ + async invalidateWsCacheForKeyStartingWith(key: string): Promise { + if (!key) { + return; + } + + this.logger.debug('Invalidate cache for key starting with: ' + key); + Object.values(this.memoryCache).filter(entry => entry.key?.startsWith(key)).forEach(entry => { + entry.expirationTime = 0; + }); + } + + /** + * Returns the URL to the documentation of the app, based on Moodle version and current language. + * + * @param page Docs page to go to. + * @returns Promise resolved with the Moodle docs URL. + */ + getDocsUrl(page?: string): Promise { + const release = this.infos?.release ? this.infos.release : undefined; + + return CoreUrlUtils.getDocsUrl(release, page); + } + + /** + * @inheritdoc + */ + async getPublicConfig(options: { readingStrategy?: CoreSitesReadingStrategy } = {}): Promise { + const method = 'tool_mobile_get_public_config'; + const cacheId = this.getCacheId(method, {}); + const cachePreSets: CoreSiteWSPreSets = { + getFromCache: true, + saveToCache: true, + emergencyCache: true, + cacheKey: this.getPublicConfigCacheKey(), + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), + }; + + if (this.offlineDisabled) { + // Offline is disabled, don't use cache. + cachePreSets.getFromCache = false; + cachePreSets.saveToCache = false; + cachePreSets.emergencyCache = false; + } + + // Check for an ongoing identical request if we're not ignoring cache. + + // Check for an ongoing identical request. + const ongoingRequest = this.getOngoingRequest(cacheId, cachePreSets); + if (ongoingRequest) { + return firstValueFrom(ongoingRequest); + } + + const subject = new Subject(); + const observable = subject.pipe( + // Return a clone of the original object, this may prevent errors if in the callback the object is modified. + map((data) => CoreUtils.clone(data)), + finalize(() => { + this.clearOngoingRequest(cacheId, cachePreSets, observable); + }), + ); + + this.setOngoingRequest(cacheId, cachePreSets, observable); + + this.getFromCache(method, {}, cachePreSets, false) + .then(cachedData => cachedData.response) + .catch(async () => { + if (cachePreSets.forceOffline) { + // Don't call the WS, just fail. + throw new CoreError(Translate.instant('core.cannotconnect')); + } + + // Call the WS. + try { + const config = await this.requestPublicConfig(); + + if (cachePreSets.saveToCache) { + this.saveToCache(method, {}, config, cachePreSets); + } + + return config; + } catch (error) { + cachePreSets.omitExpires = true; + cachePreSets.getFromCache = true; + + try { + const cachedData = await this.getFromCache(method, {}, cachePreSets, true); + + return cachedData.response; + } catch { + throw error; + } + } + }).then((response) => { + // The app doesn't store exceptions for this call, it's safe to assume type CoreSitePublicConfigResponse. + subject.next( response); + subject.complete(); + + return; + }).catch((error) => { + subject.error(error); + }); + + return firstValueFrom(observable); + } + + /** + * Get cache key for getPublicConfig WS calls. + * + * @returns Cache key. + */ + protected getPublicConfigCacheKey(): string { + return 'tool_mobile_get_public_config'; + } + + /** + * Check if GET method is supported for AJAX calls. + * + * @returns Whether it's supported. + */ + protected isAjaxGetSupported(): boolean { + return !!this.getInfo() && this.isVersionGreaterEqualThan('3.8'); + } + + /** + * Check if the site version is greater than one or several versions. + * This function accepts a string or an array of strings. If array, the last version must be the highest. + * + * @param versions Version or list of versions to check. + * @returns Whether it's greater or equal, false otherwise. + * @description + * If a string is supplied (e.g. '3.2.1'), it will check if the site version is greater or equal than this version. + * + * If an array of versions is supplied, it will check if the site version is greater or equal than the last version, + * or if it's higher or equal than any of the other releases supplied but lower than the next major release. The last + * version of the array must be the highest version. + * For example, if the values supplied are ['3.0.5', '3.2.3', '3.3.1'] the function will return true if the site version + * is either: + * - Greater or equal than 3.3.1. + * - Greater or equal than 3.2.3 but lower than 3.3. + * - Greater or equal than 3.0.5 but lower than 3.1. + * + * This function only accepts versions from 2.4.0 and above. If any of the versions supplied isn't found, it will assume + * it's the last released major version. + */ + isVersionGreaterEqualThan(versions: string | string[]): boolean { + const info = this.getInfo(); + + if (!info || !info.version) { + return false; + } + + const siteVersion = Number(info.version); + + if (Array.isArray(versions)) { + if (!versions.length) { + return false; + } + + for (let i = 0; i < versions.length; i++) { + const versionNumber = this.getVersionNumber(versions[i]); + if (i == versions.length - 1) { + // It's the last version, check only if site version is greater than this one. + return siteVersion >= versionNumber; + } else { + // Check if site version if bigger than this number but lesser than next major. + if (siteVersion >= versionNumber && siteVersion < this.getNextMajorVersionNumber(versions[i])) { + return true; + } + } + } + } else if (typeof versions == 'string') { + // Compare with this version. + return siteVersion >= this.getVersionNumber(versions); + } + + return false; + } + + /** + * Get a version number from a release version. + * If release version is valid but not found in the list of Moodle releases, it will use the last released major version. + * + * @param version Release version to convert to version number. + * @returns Version number, 0 if invalid. + */ + protected getVersionNumber(version: string): number { + const data = this.getMajorAndMinor(version); + + if (!data) { + // Invalid version. + return 0; + } + + if (CoreCandidateSite.MOODLE_RELEASES[data.major] === undefined) { + // Major version not found. Use the last one. + const major = Object.keys(CoreCandidateSite.MOODLE_RELEASES).pop(); + if (!major) { + return 0; + } + + data.major = major; + } + + return CoreCandidateSite.MOODLE_RELEASES[data.major] + data.minor; + } + + /** + * Given a release version, return the major and minor versions. + * + * @param version Release version (e.g. '3.1.0'). + * @returns Object with major and minor. Returns false if invalid version. + */ + protected getMajorAndMinor(version: string): {major: string; minor: number} | false { + const match = version.match(/^(\d+)(\.(\d+)(\.\d+)?)?/); + if (!match || !match[1]) { + // Invalid version. + return false; + } + + return { + major: match[1] + '.' + (match[3] || '0'), + minor: parseInt(match[5], 10) || 0, + }; + } + + /** + * Given a release version, return the next major version number. + * + * @param version Release version (e.g. '3.1.0'). + * @returns Next major version number. + */ + protected getNextMajorVersionNumber(version: string): number { + const data = this.getMajorAndMinor(version); + const releases = Object.keys(CoreCandidateSite.MOODLE_RELEASES); + + if (!data) { + // Invalid version. + return 0; + } + + const position = releases.indexOf(data.major); + + if (position == -1 || position == releases.length - 1) { + // Major version not found or it's the last one. Use the last one. + return CoreCandidateSite.MOODLE_RELEASES[releases[position]]; + } + + return CoreCandidateSite.MOODLE_RELEASES[releases[position + 1]]; + } + + /** + * Get a certain cache expiration delay. + * + * @param updateFrequency The update frequency of the entry. + * @returns Expiration delay. + */ + getExpirationDelay(updateFrequency?: number): number { + updateFrequency = updateFrequency || CoreCandidateSite.FREQUENCY_USUALLY; + let expirationDelay = CoreCandidateSite.UPDATE_FREQUENCIES[updateFrequency] || + CoreCandidateSite.UPDATE_FREQUENCIES[CoreCandidateSite.FREQUENCY_USUALLY]; + + if (CoreNetwork.isNetworkAccessLimited()) { + // Not WiFi, increase the expiration delay a 50% to decrease the data usage in this case. + expirationDelay *= 1.5; + } + + return expirationDelay; + } + +} + +/** + * Operator to chain requests when using observables. + * + * @param readingStrategy Reading strategy used for the current request. + * @param callback Callback called with the result of current request and the reading strategy to use in next requests. + * @returns Operator. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function chainRequests>( + readingStrategy: CoreSitesReadingStrategy | undefined, + callback: (data: T, readingStrategy?: CoreSitesReadingStrategy) => O, +): OperatorFunction> { + return (source: WSObservable) => new Observable<{ data: T; readingStrategy?: CoreSitesReadingStrategy }>(subscriber => { + let firstValue = true; + let isCompleted = false; + + return source.subscribe({ + next: async (value) => { + if (readingStrategy !== CoreSitesReadingStrategy.STALE_WHILE_REVALIDATE) { + // Just use same strategy. + subscriber.next({ data: value, readingStrategy }); + + return; + } + + if (!firstValue) { + // Second (last) value. Chained requests should have used cached data already, just return 1 value now. + subscriber.next({ + data: value, + }); + + return; + } + + firstValue = false; + + // Wait to see if the observable is completed (no more values). + await CoreUtils.nextTick(); + + if (isCompleted) { + // Current request only returns cached data. Let chained requests update in background. + subscriber.next({ data: value, readingStrategy }); + } else { + // Current request will update in background. Prefer cached data in the chained requests. + subscriber.next({ + data: value, + readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE, + }); + } + }, + error: (error) => subscriber.error(error), + complete: async () => { + isCompleted = true; + + await CoreUtils.nextTick(); + + subscriber.complete(); + }, + }); + }).pipe( + mergeMap(({ data, readingStrategy }) => callback(data, readingStrategy)), + ); +} + +/** + * PreSets accepted by the WS call. + */ +export type CoreSiteWSPreSets = { + /** + * Get the value from the cache if it's still valid. + */ + getFromCache?: boolean; + + /** + * Save the result to the cache. + */ + saveToCache?: boolean; + + /** + * Ignore cache expiration. + */ + omitExpires?: boolean; + + /** + * Use the cache when a request fails. Defaults to true. + */ + emergencyCache?: boolean; + + /** + * If true, the app won't call the WS. If the data isn't cached, the call will fail. + */ + forceOffline?: boolean; + + /** + * Extra key to add to the cache when storing this call, to identify the entry. + */ + cacheKey?: string; + + /** + * Whether it should use cache key to retrieve the cached data instead of the request params. + */ + getCacheUsingCacheKey?: boolean; + + /** + * Same as getCacheUsingCacheKey, but for emergency cache. + */ + getEmergencyCacheUsingCacheKey?: boolean; + + /** + * If true, the cache entry will be deleted if the WS call returns an exception. + */ + deleteCacheIfWSError?: boolean; + + /** + * Whether it should only be 1 entry for this cache key (all entries with same key will be deleted). + */ + uniqueCacheKey?: boolean; + + /** + * Whether to filter WS response (moodlewssettingfilter). Defaults to true. + */ + filter?: boolean; + + /** + * Whether to rewrite URLs (moodlewssettingfileurl). Defaults to true. + */ + rewriteurls?: boolean; + + /** + * Language to send to the WebService (moodlewssettinglang). Defaults to app's language. + */ + lang?: string; + + /** + * Defaults to true. Set to false when the expected response is null. + */ + responseExpected?: boolean; + + /** + * Defaults to 'object'. Use it when you expect a type that's not an object|array. + */ + typeExpected?: CoreWSTypeExpected; + + /** + * Wehther a pending request in the queue matching the same function and arguments can be reused instead of adding + * a new request to the queue. Defaults to true for read requests. + */ + reusePending?: boolean; + + /** + * Whether the request will be be sent immediately as a single request. Defaults to false. + */ + skipQueue?: boolean; + + /** + * Cache the response if it returns an errorcode present in this list. + */ + cacheErrors?: string[]; + + /** + * Update frequency. This value determines how often the cached data will be updated. Possible values: + * CoreSite.FREQUENCY_USUALLY, CoreSite.FREQUENCY_OFTEN, CoreSite.FREQUENCY_SOMETIMES, CoreSite.FREQUENCY_RARELY. + * Defaults to CoreSite.FREQUENCY_USUALLY. + */ + updateFrequency?: number; + + /** + * Component name. Optionally included if this request is being made on behalf of a specific + * component (e.g. activity). + */ + component?: string; + + /** + * Component id. Optionally included when 'component' is set. + */ + componentId?: number; + + /** + * Whether to split a request if it has too many parameters. Sending too many parameters to the site + * can cause the request to fail (see PHP's max_input_vars). + */ + splitRequest?: CoreWSPreSetsSplitRequest; + + /** + * If true, the app will return cached data even if it's expired and then it'll call the WS in the background. + * Only enabled if CoreConstants.CONFIG.disableCallWSInBackground isn't true. + */ + updateInBackground?: boolean; +}; + +/** + * Info of a request waiting in the queue. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type RequestQueueItem = { + cacheId: string; + method: string; + data: any; // eslint-disable-line @typescript-eslint/no-explicit-any + preSets: CoreSiteWSPreSets; + wsPreSets: CoreWSPreSets; + deferred: CorePromisedValue; +}; + +/** + * Result of WS tool_mobile_call_external_functions. + */ +export type CoreSiteCallExternalFunctionsResult = { + responses: { + error: boolean; // Whether an exception was thrown. + data?: string; // JSON-encoded response data. + exception?: string; // JSON-encoed exception info. + }[]; +}; + +/** + * Info about cached data. + */ +type WSCachedData = { + response: T | WSCachedError; // The WS response data, or an error if the WS returned an error and it was cached. + expirationIgnored: boolean; // Whether the expiration time was ignored. + expirationTime?: number; // Entry expiration time (only if not ignored). +}; + +/** + * Error data stored in cache. + */ +type WSCachedError = { + exception?: string; + errorcode?: string; +}; + +/** + * Observable returned when calling WebServices. + * If the request uses the "update in background" feature, it will return 2 values: first the cached one, and then the one + * coming from the server. After this, it will complete. + * Otherwise, it will only return 1 value, either coming from cache or from the server. After this, it will complete. + */ +export type WSObservable = Observable; + +/** + * Type of ongoing requests stored in memory to avoid duplicating them. + */ +enum OngoingRequestType { + STANDARD = 0, + UPDATE_IN_BACKGROUND = 1, +} diff --git a/src/core/classes/sites/site.ts b/src/core/classes/sites/site.ts index 9e461dd25..90f9ff452 100644 --- a/src/core/classes/sites/site.ts +++ b/src/core/classes/sites/site.ts @@ -13,23 +13,18 @@ // limitations under the License. import { InAppBrowserObject, InAppBrowserOptions } from '@ionic-native/in-app-browser'; -import { Md5 } from 'ts-md5/dist/md5'; -import { CoreApp } from '@services/app'; import { CoreNetwork } from '@services/network'; import { CoreDB } from '@services/db'; import { CoreEvents } from '@singletons/events'; import { CoreFile } from '@services/file'; import { CoreWS, - CoreWSPreSets, CoreWSFileUploadOptions, CoreWSExternalWarning, CoreWSUploadFileResult, - CoreWSPreSetsSplitRequest, - CoreWSTypeExpected, } from '@services/ws'; -import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; +import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrlUtils } from '@services/utils/url'; @@ -37,17 +32,13 @@ import { CoreUtils, CoreUtilsOpenInBrowserOptions } from '@services/utils/utils' import { CoreConstants } from '@/core/constants'; import { SQLiteDB } from '@classes/sqlitedb'; import { CoreError } from '@classes/errors/error'; -import { CoreWSError } from '@classes/errors/wserror'; import { CoreLogger } from '@singletons/logger'; import { Translate } from '@singletons'; import { CoreIonLoadingElement } from '../ion-loading'; -import { CoreLang, CoreLangFormat } from '@services/lang'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { asyncInstance, AsyncInstance } from '../../utils/async-instance'; import { CoreDatabaseTable } from '../database/database-table'; import { CoreDatabaseCachingStrategy } from '../database/database-table-proxy'; -import { CoreSilentError } from '../errors/silenterror'; -import { CorePromisedValue } from '@classes/promised-value'; import { CONFIG_TABLE, CoreSiteConfigDBRecord, @@ -56,76 +47,27 @@ import { LAST_VIEWED_TABLE, WS_CACHE_TABLE, } from '@services/database/sites'; -import { Observable, ObservableInput, ObservedValueOf, OperatorFunction, Subject } from 'rxjs'; -import { finalize, map, mergeMap } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { firstValueFrom } from '../../utils/rxjs'; -import { CoreSiteError } from '@classes/errors/siteerror'; -import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config'; -import { CoreErrorLogs } from '@singletons/error-logs'; import { CoreFilepool } from '@services/filepool'; -import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse, CoreUnauthenticatedSite } from './unauthenticated-site'; - -// WS that we allow to call even if the site is logged out. -const ALLOWED_LOGGEDOUT_WS = [ - 'core_user_remove_user_device', -]; +import { CoreSiteInfo } from './unauthenticated-site'; +import { CoreCandidateSite, CoreSiteWSPreSets, WSObservable } from './candidate-site'; /** * Class that represents a site (combination of site + user). * It will have all the site data and provide utility functions regarding a site. - * To add tables to the site's database, please use registerSiteSchema exported in @services/sites.ts. This will make sure that - * the tables are created in all the sites, not just the current one. - * - * @todo Refactor this class to improve "temporary" sites support (not fully authenticated). */ -export class CoreSite extends CoreUnauthenticatedSite { +export class CoreSite extends CoreCandidateSite { - static readonly REQUEST_QUEUE_FORCE_WS = false; // Use "tool_mobile_call_external_functions" even for calling a single function. + id: string; + config?: CoreSiteConfig; + loggedOut?: boolean; - // Constants for cache update frequency. - static readonly FREQUENCY_USUALLY = 0; - static readonly FREQUENCY_OFTEN = 1; - static readonly FREQUENCY_SOMETIMES = 2; - static readonly FREQUENCY_RARELY = 3; - - static readonly MINIMUM_MOODLE_VERSION = '3.5'; - - // Versions of Moodle releases. - static readonly MOODLE_RELEASES = { - '3.5': 2018051700, - '3.6': 2018120300, - '3.7': 2019052000, - '3.8': 2019111800, - '3.9': 2020061500, - '3.10': 2020110900, - '3.11': 2021051700, - '4.0': 2022041900, - '4.1': 2022112800, - '4.2': 2023042400, - '4.3': 2023100900, - }; - - // Possible cache update frequencies. - protected readonly UPDATE_FREQUENCIES = [ - CoreConstants.CONFIG.cache_update_frequency_usually || 420000, - CoreConstants.CONFIG.cache_update_frequency_often || 1200000, - CoreConstants.CONFIG.cache_update_frequency_sometimes || 3600000, - CoreConstants.CONFIG.cache_update_frequency_rarely || 43200000, - ]; - - // Rest of variables. - protected logger: CoreLogger; - protected db?: SQLiteDB; + protected db!: SQLiteDB; protected cacheTable: AsyncInstance>; protected configTable: AsyncInstance>; protected lastViewedTable: AsyncInstance>; - protected cleanUnicode = false; protected lastAutoLogin = 0; - protected offlineDisabled = false; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - protected ongoingRequests: Record | undefined>> = {}; - protected requestQueue: RequestQueueItem[] = []; - protected requestQueueTimeout: number | null = null; protected tokenPluginFileWorks?: boolean; protected tokenPluginFileWorksPromise?: Promise; protected oauthId?: number; @@ -142,16 +84,19 @@ export class CoreSite extends CoreUnauthenticatedSite { * @param loggedOut Whether user is logged out. */ constructor( - public id: string | undefined, - public siteUrl: string, - public token?: string, - public infos?: CoreSiteInfo, - public privateToken?: string, - public config?: CoreSiteConfig, - public loggedOut?: boolean, + id: string, + siteUrl: string, + token: string, + infos?: CoreSiteInfo, + privateToken?: string, + config?: CoreSiteConfig, + loggedOut?: boolean, ) { - super(siteUrl); + super(siteUrl, token, privateToken); + this.id = id; + this.config = config; + this.loggedOut = loggedOut; this.logger = CoreLogger.getInstance('CoreSite'); this.cacheTable = asyncInstance(() => CoreSites.getSiteTable(WS_CACHE_TABLE, { @@ -176,15 +121,6 @@ export class CoreSite extends CoreUnauthenticatedSite { this.setInfo(infos); this.calculateOfflineDisabled(); - if (this.id) { - this.initDB(); - } - } - - /** - * Initialize the database. - */ - initDB(): void { this.db = CoreDB.getDB('Site-' + this.id); } @@ -194,109 +130,18 @@ export class CoreSite extends CoreUnauthenticatedSite { * @returns Site ID. */ getId(): string { - if (this.id === undefined) { - // Shouldn't happen for authenticated sites. - throw new CoreError('This site doesn\'t have an ID'); - } - return this.id; } - /** - * Get site token. - * - * @returns Site token. - */ - getToken(): string { - if (this.token === undefined) { - // Shouldn't happen for authenticated sites. - throw new CoreError('This site doesn\'t have a token'); - } - - return this.token; - } - - /** - * @inheritdoc - */ - getInfo(): CoreSiteInfo | undefined { - return this.infos; - } - - /** - * Get site private token. - * - * @returns Site private token. - */ - getPrivateToken(): string | undefined { - return this.privateToken; - } - /** * Get site DB. * * @returns Site DB. */ getDb(): SQLiteDB { - if (!this.db) { - // Shouldn't happen for authenticated sites. - throw new CoreError('Site DB doesn\'t exist'); - } - return this.db; } - /** - * Get site user's ID. - * - * @returns User's ID. - */ - getUserId(): number { - if (!this.infos) { - // Shouldn't happen for authenticated sites. - throw new CoreError('Site info could not be fetched.'); - } - - return this.infos.userid; - } - - /** - * Get site Course ID for frontpage course. If not declared it will return 1 as default. - * - * @returns Site Home ID. - */ - getSiteHomeId(): number { - return this.infos?.siteid || 1; - } - - /** - * Set site ID. - * - * @param id New ID. - */ - setId(id: string): void { - this.id = id; - this.initDB(); - } - - /** - * Set site token. - * - * @param token New token. - */ - setToken(token: string): void { - this.token = token; - } - - /** - * Set site private token. - * - * @param privateToken New private token. - */ - setPrivateToken(privateToken: string): void { - this.privateToken = privateToken; - } - /** * Check if user logged out from the site and needs to authenticate again. * @@ -315,20 +160,6 @@ export class CoreSite extends CoreUnauthenticatedSite { return this.oauthId; } - /** - * Set site info. - * - * @param infos New info. - */ - setInfo(infos?: CoreSiteInfo): void { - this.infos = infos; - - // Index function by name to speed up wsAvailable method. - if (infos?.functions) { - infos.functionsByName = CoreUtils.arrayToObject(infos.functions, 'name'); - } - } - /** * Set site config. * @@ -361,16 +192,6 @@ export class CoreSite extends CoreUnauthenticatedSite { this.oauthId = oauthId; } - /** - * Check if current user is Admin. - * Works properly since v3.8. See more in: {@link} https://tracker.moodle.org/browse/MDL-65550 - * - * @returns Whether the user is Admin. - */ - isAdmin(): boolean { - return this.getInfo()?.userissiteadmin ?? false; - } - /** * Check if the user authenticated in the site using an OAuth method. * @@ -381,883 +202,36 @@ export class CoreSite extends CoreUnauthenticatedSite { } /** - * Can the user access their private files? - * - * @returns Whether can access my files. + * @inheritdoc */ - canAccessMyFiles(): boolean { - const info = this.getInfo(); - - return !!(info && (info.usercanmanageownfiles === undefined || info.usercanmanageownfiles)); + protected async getCacheEntryById(id: string): Promise { + return this.cacheTable.getOneByPrimaryKey({ id }); } /** - * Can the user download files? - * - * @returns Whether can download files. + * @inheritdoc */ - canDownloadFiles(): boolean { - const info = this.getInfo(); - - return !!info?.downloadfiles && info?.downloadfiles > 0; + protected async getCacheEntriesByKey(key: string): Promise { + return this.cacheTable.getMany({ key }); } /** - * Can the user use an advanced feature? - * - * @param featureName The name of the feature. - * @param whenUndefined The value to return when the parameter is undefined. - * @returns Whether can use advanced feature. + * @inheritdoc */ - canUseAdvancedFeature(featureName: string, whenUndefined: boolean = true): boolean { - const info = this.getInfo(); - - if (info?.advancedfeatures === undefined) { - return whenUndefined; - } - - const feature = info.advancedfeatures.find((item) => item.name === featureName); - - if (!feature) { - return whenUndefined; - } - - return feature.value !== 0; + protected async storeCacheEntry(entry: CoreSiteWSCacheRecord): Promise { + await this.cacheTable.insert(entry); } /** - * Can the user upload files? - * - * @returns Whether can upload files. - */ - canUploadFiles(): boolean { - const info = this.getInfo(); - - return !!info?.uploadfiles && info?.uploadfiles > 0; - } - - /** - * Fetch site info from the Moodle site. - * - * @returns A promise to be resolved when the site info is retrieved. - */ - fetchSiteInfo(): Promise { - // The get_site_info WS call won't be cached. - const preSets = { - getFromCache: false, - saveToCache: false, - skipQueue: true, - }; - - // Reset clean Unicode to check if it's supported again. - this.cleanUnicode = false; - - return this.read('core_webservice_get_site_info', {}, preSets); - } - - /** - * Read some data from the Moodle site using WS. Requests are cached by default. - * - * @param method WS method to use. - * @param data Data to send to the WS. - * @param preSets Extra options. - * @returns Promise resolved with the response, rejected with CoreWSError if it fails. + * @inheritdoc */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - read(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise { - return firstValueFrom(this.readObservable(method, data, preSets)); - } - - /** - * Read some data from the Moodle site using WS. Requests are cached by default. - * - * @param method WS method to use. - * @param data Data to send to the WS. - * @param preSets Extra options. - * @returns Observable returning the WS data. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readObservable(method: string, data: any, preSets?: CoreSiteWSPreSets): WSObservable { - preSets = preSets || {}; - preSets.getFromCache = preSets.getFromCache ?? true; - preSets.saveToCache = preSets.saveToCache ?? true; - preSets.reusePending = preSets.reusePending ?? true; - - return this.requestObservable(method, data, preSets); - } - - /** - * Sends some data to the Moodle site using WS. Requests are NOT cached by default. - * - * @param method WS method to use. - * @param data Data to send to the WS. - * @param preSets Extra options. - * @returns Promise resolved with the response, rejected with CoreWSError if it fails. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - write(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise { - return firstValueFrom(this.writeObservable(method, data, preSets)); - } - - /** - * Sends some data to the Moodle site using WS. Requests are NOT cached by default. - * - * @param method WS method to use. - * @param data Data to send to the WS. - * @param preSets Extra options. - * @returns Observable returning the WS data. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - writeObservable(method: string, data: any, preSets?: CoreSiteWSPreSets): WSObservable { - preSets = preSets || {}; - preSets.getFromCache = preSets.getFromCache ?? false; - preSets.saveToCache = preSets.saveToCache ?? false; - preSets.emergencyCache = preSets.emergencyCache ?? false; - - return this.requestObservable(method, data, preSets); - } - - /** - * WS request to the site. - * - * @param method The WebService method to be called. - * @param data Arguments to pass to the method. - * @param preSets Extra options. - * @returns Promise resolved with the response, rejected with CoreWSError if it fails. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async request(method: string, data: any, preSets: CoreSiteWSPreSets): Promise { - return firstValueFrom(this.requestObservable(method, data, preSets)); - } - - /** - * WS request to the site. - * - * @param method The WebService method to be called. - * @param data Arguments to pass to the method. - * @param preSets Extra options. - * @returns Observable returning the WS data. - * @description - * - * Sends a webservice request to the site. This method will automatically add the - * required parameters and pass it on to the low level API in CoreWSProvider.call(). - * - * Caching is also implemented, when enabled this method will returned a cached version of the request if the - * data hasn't expired. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - requestObservable(method: string, data: any, preSets: CoreSiteWSPreSets): WSObservable { - if (this.isLoggedOut() && !ALLOWED_LOGGEDOUT_WS.includes(method)) { - // Site is logged out, it cannot call WebServices. - CoreEvents.trigger(CoreEvents.SESSION_EXPIRED, {}, this.id); - - // Use a silent error, the SESSION_EXPIRED event will display a message if needed. - throw new CoreSilentError(Translate.instant('core.lostconnection')); - } - - data = data || {}; - - if (!CoreNetwork.isOnline() && this.offlineDisabled) { - throw new CoreError(Translate.instant('core.errorofflinedisabled')); - } - - // Check if the method is available. - // We ignore this check when we do not have the site info, as the list of functions is not loaded yet. - if (this.getInfo() && !this.wsAvailable(method)) { - this.logger.error(`WS function '${method}' is not available.`); - - throw new CoreError(Translate.instant('core.wsfunctionnotavailable')); - } - - const wsPreSets: CoreWSPreSets = { - wsToken: this.token || '', - siteUrl: this.siteUrl, - cleanUnicode: this.cleanUnicode, - typeExpected: preSets.typeExpected, - responseExpected: preSets.responseExpected, - splitRequest: preSets.splitRequest, - }; - - if (wsPreSets.cleanUnicode && CoreTextUtils.hasUnicodeData(data)) { - // Data will be cleaned, notify the user. - CoreDomUtils.showToast('core.unicodenotsupported', true, ToastDuration.LONG); + protected async deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise { + if (allCacheKey) { + await this.cacheTable.delete({ key: preSets.cacheKey }); } else { - // No need to clean data in this call. - wsPreSets.cleanUnicode = false; + await this.cacheTable.deleteByPrimaryKey({ id: this.getCacheId(method, data) }); } - - if (this.offlineDisabled) { - // Offline is disabled, don't use cache. - preSets.getFromCache = false; - preSets.saveToCache = false; - preSets.emergencyCache = false; - } - - // Enable text filtering by default. - data.moodlewssettingfilter = preSets.filter === false ? false : true; - data.moodlewssettingfileurl = preSets.rewriteurls === false ? false : true; - - // Convert arguments to strings before starting the cache process. - data = CoreWS.convertValuesToString(data, wsPreSets.cleanUnicode); - if (data == null) { - // Empty cleaned text found. - throw new CoreError(Translate.instant('core.unicodenotsupportedcleanerror')); - } - - const cacheId = this.getCacheId(method, data); - - // Check for an ongoing identical request. - const ongoingRequest = this.getOngoingRequest(cacheId, preSets); - if (ongoingRequest) { - return ongoingRequest; - } - - const observable = this.performRequest(method, data, preSets, wsPreSets).pipe( - // Return a clone of the original object, this may prevent errors if in the callback the object is modified. - map((data) => CoreUtils.clone(data)), - ); - - this.setOngoingRequest(cacheId, preSets, observable); - - return observable.pipe( - finalize(() => { - this.clearOngoingRequest(cacheId, preSets, observable); - }), - ); - } - - /** - * Get an ongoing request if there's one already. - * - * @param cacheId Cache ID. - * @param preSets Presets. - * @returns Ongoing request if it exists. - */ - protected getOngoingRequest(cacheId: string, preSets: CoreSiteWSPreSets): WSObservable | undefined { - if (preSets.updateInBackground) { - return this.ongoingRequests[cacheId]?.[OngoingRequestType.UPDATE_IN_BACKGROUND]; - } else if (preSets.getFromCache) { // Only reuse ongoing request when using cache. - return this.ongoingRequests[cacheId]?.[OngoingRequestType.STANDARD]; - } - } - - /** - * Store an ongoing request in memory. - * - * @param cacheId Cache ID. - * @param preSets Presets. - * @param request Request to store. - */ - protected setOngoingRequest(cacheId: string, preSets: CoreSiteWSPreSets, request: WSObservable): void { - this.ongoingRequests[cacheId] = this.ongoingRequests[cacheId] ?? {}; - - if (preSets.updateInBackground) { - this.ongoingRequests[cacheId][OngoingRequestType.UPDATE_IN_BACKGROUND] = request; - } else { - this.ongoingRequests[cacheId][OngoingRequestType.STANDARD] = request; - } - } - - /** - * Clear the ongoing request unless it has changed (e.g. a new request that ignores cache). - * - * @param cacheId Cache ID. - * @param preSets Presets. - * @param request Current request. - */ - protected clearOngoingRequest(cacheId: string, preSets: CoreSiteWSPreSets, request: WSObservable): void { - this.ongoingRequests[cacheId] = this.ongoingRequests[cacheId] ?? {}; - - if (preSets.updateInBackground) { - if (this.ongoingRequests[cacheId][OngoingRequestType.UPDATE_IN_BACKGROUND] === request) { - delete this.ongoingRequests[cacheId][OngoingRequestType.UPDATE_IN_BACKGROUND]; - } - } else { - if (this.ongoingRequests[cacheId][OngoingRequestType.STANDARD] === request) { - delete this.ongoingRequests[cacheId][OngoingRequestType.STANDARD]; - } - } - } - - /** - * Perform a request, getting the response either from cache or WebService. - * - * @param method The WebService method to be called. - * @param data Arguments to pass to the method. - * @param preSets Extra options related to the site. - * @param wsPreSets Extra options related to the WS call. - * @returns Observable returning the WS data. - */ - protected performRequest( - method: string, - data: unknown, - preSets: CoreSiteWSPreSets, - wsPreSets: CoreWSPreSets, - ): WSObservable { - const subject = new Subject(); - - const run = async () => { - try { - let response: T | WSCachedError; - let cachedData: WSCachedData | undefined; - - try { - cachedData = await this.getFromCache(method, data, preSets, false); - response = cachedData.response; - } catch { - // Not found or expired, call WS. - response = await this.getFromWS(method, data, preSets, wsPreSets); - } - - if ( - typeof response === 'object' && response !== null && - ( - ('exception' in response && response.exception !== undefined) || - ('errorcode' in response && response.errorcode !== undefined) - ) - ) { - subject.error(new CoreWSError(response)); - } else { - subject.next( response); - } - - if ( - preSets.updateInBackground && - !CoreConstants.CONFIG.disableCallWSInBackground && - cachedData && - !cachedData.expirationIgnored && - cachedData.expirationTime !== undefined && - Date.now() > cachedData.expirationTime - ) { - // Update the data in background. - setTimeout(async () => { - try { - preSets = { - ...preSets, - emergencyCache: false, - }; - - const newData = await this.getFromWS(method, data, preSets, wsPreSets); - - subject.next(newData); - } catch (error) { - // Ignore errors when updating in background. - this.logger.error('Error updating WS data in background', error); - } finally { - subject.complete(); - } - }); - } else { - // No need to update in background, complete the observable. - subject.complete(); - } - } catch (error) { - subject.error(error); - } - }; - - run(); - - return subject; - } - - /** - * Get a request response from WS, if it fails it might try to get it from emergency cache. - * - * @param method The WebService method to be called. - * @param data Arguments to pass to the method. - * @param preSets Extra options related to the site. - * @param wsPreSets Extra options related to the WS call. - * @returns Promise resolved with the response. - */ - protected async getFromWS( - method: string, - data: any, // eslint-disable-line @typescript-eslint/no-explicit-any - preSets: CoreSiteWSPreSets, - wsPreSets: CoreWSPreSets, - ): Promise { - if (preSets.forceOffline) { - // Don't call the WS, just fail. - throw new CoreError(Translate.instant('core.cannotconnect')); - } - - try { - const response = await this.callOrEnqueueWS(method, data, preSets, wsPreSets); - - if (preSets.saveToCache) { - this.saveToCache(method, data, response, preSets); - } - - return response; - } catch (error) { - let useSilentError = false; - - if (CoreUtils.isExpiredTokenError(error)) { - // Session expired, trigger event. - CoreEvents.trigger(CoreEvents.SESSION_EXPIRED, {}, this.id); - // Change error message. Try to get data from cache, the event will handle the error. - error.message = Translate.instant('core.lostconnection'); - useSilentError = true; // Use a silent error, the SESSION_EXPIRED event will display a message if needed. - } else if (error.errorcode === 'userdeleted' || error.errorcode === 'wsaccessuserdeleted') { - // User deleted, trigger event. - CoreEvents.trigger(CoreEvents.USER_DELETED, { params: data }, this.id); - error.message = Translate.instant('core.userdeleted'); - - throw new CoreWSError(error); - } else if (error.errorcode === 'wsaccessusersuspended') { - // User suspended, trigger event. - CoreEvents.trigger(CoreEvents.USER_SUSPENDED, { params: data }, this.id); - error.message = Translate.instant('core.usersuspended'); - - throw new CoreWSError(error); - } else if (error.errorcode === 'wsaccessusernologin') { - // User suspended, trigger event. - CoreEvents.trigger(CoreEvents.USER_NO_LOGIN, { params: data }, this.id); - error.message = Translate.instant('core.usernologin'); - - throw new CoreWSError(error); - } else if (error.errorcode === 'forcepasswordchangenotice') { - // Password Change Forced, trigger event. Try to get data from cache, the event will handle the error. - CoreEvents.trigger(CoreEvents.PASSWORD_CHANGE_FORCED, {}, this.id); - error.message = Translate.instant('core.forcepasswordchangenotice'); - useSilentError = true; // Use a silent error, the change password page already displays the appropiate info. - } else if (error.errorcode === 'usernotfullysetup') { - // User not fully setup, trigger event. Try to get data from cache, the event will handle the error. - CoreEvents.trigger(CoreEvents.USER_NOT_FULLY_SETUP, {}, this.id); - error.message = Translate.instant('core.usernotfullysetup'); - useSilentError = true; // Use a silent error, the complete profile page already displays the appropiate info. - } else if (error.errorcode === 'sitepolicynotagreed') { - // Site policy not agreed, trigger event. - CoreEvents.trigger(CoreEvents.SITE_POLICY_NOT_AGREED, {}, this.id); - error.message = Translate.instant('core.login.sitepolicynotagreederror'); - - throw new CoreWSError(error); - } else if (error.errorcode === 'dmlwriteexception' && CoreTextUtils.hasUnicodeData(data)) { - if (!this.cleanUnicode) { - // Try again cleaning unicode. - this.cleanUnicode = true; - - return this.request(method, data, preSets); - } - // This should not happen. - error.message = Translate.instant('core.unicodenotsupported'); - - throw new CoreWSError(error); - } else if (error.exception === 'required_capability_exception' || error.errorcode === 'nopermission' || - error.errorcode === 'notingroup') { - // Translate error messages with missing strings. - if (error.message === 'error/nopermission') { - error.message = Translate.instant('core.nopermissionerror'); - } else if (error.message === 'error/notingroup') { - error.message = Translate.instant('core.notingroup'); - } - - if (preSets.saveToCache) { - // Save the error instead of deleting the cache entry so the same content is displayed in offline. - this.saveToCache(method, data, error, preSets); - } - - throw new CoreWSError(error); - } else if (preSets.cacheErrors && preSets.cacheErrors.indexOf(error.errorcode) != -1) { - // Save the error instead of deleting the cache entry so the same content is displayed in offline. - this.saveToCache(method, data, error, preSets); - - throw new CoreWSError(error); - } else if (preSets.emergencyCache !== undefined && !preSets.emergencyCache) { - this.logger.debug(`WS call '${method}' failed. Emergency cache is forbidden, rejecting.`); - - throw new CoreWSError(error); - } - - if (preSets.deleteCacheIfWSError && CoreUtils.isWebServiceError(error)) { - // Delete the cache entry and return the entry. Don't block the user with the delete. - CoreUtils.ignoreErrors(this.deleteFromCache(method, data, preSets)); - - throw new CoreWSError(error); - } - - this.logger.debug(`WS call '${method}' failed. Trying to use the emergency cache.`); - preSets = { - ...preSets, - omitExpires: true, - getFromCache: true, - }; - - try { - const cachedData = await this.getFromCache(method, data, preSets, true); - - if ( - typeof cachedData.response === 'object' && cachedData.response !== null && - ( - ('exception' in cachedData.response && cachedData.response.exception !== undefined) || - ('errorcode' in cachedData.response && cachedData.response.errorcode !== undefined) - ) - ) { - throw new CoreWSError(cachedData.response); - } - - return cachedData.response; - } catch { - if (useSilentError) { - throw new CoreSilentError(error.message); - } - - throw new CoreWSError(error); - } - } - } - - /** - * Get a request response from WS. - * - * @param method The WebService method to be called. - * @param data Arguments to pass to the method. - * @param preSets Extra options related to the site. - * @param wsPreSets Extra options related to the WS call. - * @returns Promise resolved with the response. - */ - protected async callOrEnqueueWS( - method: string, - data: any, // eslint-disable-line @typescript-eslint/no-explicit-any - preSets: CoreSiteWSPreSets, - wsPreSets: CoreWSPreSets, - ): Promise { - // Call the WS. - const initialToken = this.token ?? ''; - - // Send the language to use. Do it after checking cache to prevent losing offline data when changing language. - // Moodle uses underscore instead of dash. - data = { - ...data, - moodlewssettinglang: CoreLang.formatLanguage(preSets.lang ?? await CoreLang.getCurrentLanguage(), CoreLangFormat.LMS), - }; - - try { - return await this.callOrEnqueueRequest(method, data, preSets, wsPreSets); - } catch (error) { - if (CoreUtils.isExpiredTokenError(error)) { - if (initialToken !== this.token) { - // Token has changed, retry with the new token. - wsPreSets.wsToken = this.token ?? ''; - - return await this.callOrEnqueueRequest(method, data, preSets, wsPreSets); - } else if (CoreApp.isSSOAuthenticationOngoing()) { - // There's an SSO authentication ongoing, wait for it to finish and try again. - await CoreApp.waitForSSOAuthentication(); - - return await this.callOrEnqueueRequest(method, data, preSets, wsPreSets); - } - } - - if (error?.errorcode === 'invalidparameter' && method === 'core_webservice_get_site_info') { - // Retry without passing the lang, this parameter isn't supported in 3.4 or older sites - // and we need this WS call to be able to determine if the site is supported or not. - delete data.moodlewssettinglang; - - return await this.callOrEnqueueRequest(method, data, preSets, wsPreSets); - } - - throw error; - } - } - - /** - * Adds a request to the queue or calls it immediately when not using the queue. - * - * @param method The WebService method to be called. - * @param data Arguments to pass to the method. - * @param preSets Extra options related to the site. - * @param wsPreSets Extra options related to the WS call. - * @returns Promise resolved with the response when the WS is called. - */ - protected callOrEnqueueRequest( - method: string, - data: any, // eslint-disable-line @typescript-eslint/no-explicit-any - preSets: CoreSiteWSPreSets, - wsPreSets: CoreWSPreSets, - ): Promise { - if (preSets.skipQueue || !this.wsAvailable('tool_mobile_call_external_functions')) { - return CoreWS.call(method, data, wsPreSets); - } - - const cacheId = this.getCacheId(method, data); - - // Check if there is an identical request waiting in the queue (read requests only by default). - if (preSets.reusePending) { - const request = this.requestQueue.find((request) => request.cacheId == cacheId); - if (request) { - return request.deferred; - } - } - - const request: RequestQueueItem = { - cacheId, - method, - data, - preSets, - wsPreSets, - deferred: new CorePromisedValue(), - }; - - return this.enqueueRequest(request); - } - - /** - * Adds a request to the queue. - * - * @param request The request to enqueue. - * @returns Promise resolved with the response when the WS is called. - */ - protected enqueueRequest(request: RequestQueueItem): Promise { - this.requestQueue.push(request); - - if (this.requestQueue.length >= CoreConstants.CONFIG.wsrequestqueuelimit) { - this.processRequestQueue(); - } else if (!this.requestQueueTimeout) { - this.requestQueueTimeout = window.setTimeout( - () => this.processRequestQueue(), - CoreConstants.CONFIG.wsrequestqueuedelay, - ); - } - - return request.deferred; - } - - /** - * Call the enqueued web service requests. - */ - protected async processRequestQueue(): Promise { - this.logger.debug(`Processing request queue (${this.requestQueue.length} requests)`); - - // Clear timeout if set. - if (this.requestQueueTimeout) { - clearTimeout(this.requestQueueTimeout); - this.requestQueueTimeout = null; - } - - // Extract all requests from the queue. - const requests = this.requestQueue; - this.requestQueue = []; - - if (requests.length == 1 && !CoreSite.REQUEST_QUEUE_FORCE_WS) { - // Only one request, do a regular web service call. - try { - const data = await CoreWS.call(requests[0].method, requests[0].data, requests[0].wsPreSets); - - requests[0].deferred.resolve(data); - } catch (error) { - requests[0].deferred.reject(error); - } - - return; - } - - let lang: string | undefined; - const requestsData: Record = { - requests: requests.map((request) => { - const args = {}; - const settings = {}; - - // Separate WS settings from function arguments. - Object.keys(request.data).forEach((key) => { - let value = request.data[key]; - const match = /^moodlews(setting.*)$/.exec(key); - if (match) { - if (match[1] == 'settingfilter' || match[1] == 'settingfileurl') { - // Undo special treatment of these settings in CoreWSProvider.convertValuesToString. - value = (value == 'true' ? '1' : '0'); - } else if (match[1] == 'settinglang') { - // Use the lang globally to avoid exceptions with languages not installed. - lang = value; - - return; - } - settings[match[1]] = value; - } else { - args[key] = value; - } - }); - - return { - function: request.method, - arguments: JSON.stringify(args), - ...settings, - }; - }), - }; - requestsData.moodlewssettinglang = lang; - - const wsPresets: CoreWSPreSets = { - siteUrl: this.siteUrl, - wsToken: this.token || '', - }; - - try { - const data = await CoreWS.call( - 'tool_mobile_call_external_functions', - requestsData, - wsPresets, - ); - - if (!data || !data.responses) { - throw new CoreSiteError({ - supportConfig: new CoreUserAuthenticatedSupportConfig(this), - message: Translate.instant('core.siteunavailablehelp', { site: this.siteUrl }), - errorcode: 'invalidresponse', - errorDetails: Translate.instant('core.errorinvalidresponse', { method: 'tool_mobile_call_external_functions' }), - }); - } - - requests.forEach((request, i) => { - const response = data.responses[i]; - - if (!response) { - // Request not executed, enqueue again. - this.enqueueRequest(request); - } else if (response.error) { - const rejectReason = CoreTextUtils.parseJSON(response.exception || '') as Error | undefined; - request.deferred.reject(rejectReason); - CoreErrorLogs.addErrorLog({ - method: request.method, - type: 'CoreSiteError', - message: response.exception ?? '', - time: new Date().getTime(), - data: request.data, - }); - } else { - let responseData = response.data ? CoreTextUtils.parseJSON(response.data) : {}; - // Match the behaviour of CoreWSProvider.call when no response is expected. - const responseExpected = wsPresets.responseExpected === undefined || wsPresets.responseExpected; - if (!responseExpected && (responseData == null || responseData === '')) { - responseData = {}; - } - request.deferred.resolve(responseData); - } - }); - } catch (error) { - // Error not specific to a single request, reject all promises. - requests.forEach((request) => { - CoreErrorLogs.addErrorLog({ - method: request.method, - type: 'CoreSiteError', - message: String(error) ?? '', - time: new Date().getTime(), - data: request.data, - }); - request.deferred.reject(error); - }); - } - } - - /** - * Check if a WS is available in this site. - * - * @param method WS name. - * @returns Whether the WS is available. - */ - wsAvailable(method: string): boolean { - return !!this.infos?.functionsByName?.[method]; - } - - /** - * Get cache ID. - * - * @param method The WebService method. - * @param data Arguments to pass to the method. - * @returns Cache ID. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - protected getCacheId(method: string, data: any): string { - return Md5.hashAsciiStr(method + ':' + CoreUtils.sortAndStringify(data)); - } - - /** - * Get a WS response from cache. - * - * @param method The WebService method to be called. - * @param data Arguments to pass to the method. - * @param preSets Extra options. - * @param emergency Whether it's an "emergency" cache call (WS call failed). - * @returns Cached data. - */ - protected async getFromCache( - method: string, - data: any, // eslint-disable-line @typescript-eslint/no-explicit-any - preSets: CoreSiteWSPreSets, - emergency?: boolean, - ): Promise> { - if (!this.db || !preSets.getFromCache) { - throw new CoreError('Get from cache is disabled.'); - } - - const id = this.getCacheId(method, data); - let entry: CoreSiteWSCacheRecord | undefined; - - if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) { - const entries = await this.cacheTable.getMany({ key: preSets.cacheKey }); - - if (!entries.length) { - // Cache key not found, get by params sent. - entry = await this.cacheTable.getOneByPrimaryKey({ id }); - } else { - if (entries.length > 1) { - // More than one entry found. Search the one with same ID as this call. - entry = entries.find((entry) => entry.id == id); - } - - if (!entry) { - entry = entries[0]; - } - } - } else { - entry = await this.cacheTable.getOneByPrimaryKey({ id }); - } - - if (entry === undefined) { - throw new CoreError('Cache entry not valid.'); - } - - const now = Date.now(); - let expirationTime: number | undefined; - - const forceCache = preSets.omitExpires || preSets.forceOffline || !CoreNetwork.isOnline(); - - if (!forceCache) { - expirationTime = entry.expirationTime + this.getExpirationDelay(preSets.updateFrequency); - - if (preSets.updateInBackground && !CoreConstants.CONFIG.disableCallWSInBackground) { - // Use a extended expiration time. - const extendedTime = entry.expirationTime + - (CoreConstants.CONFIG.callWSInBackgroundExpirationTime ?? CoreConstants.SECONDS_WEEK * 1000); - - if (now > extendedTime) { - this.logger.debug('Cached element found, but it is expired even for call WS in background.'); - - throw new CoreError('Cache entry is expired.'); - } - } else if (now > expirationTime) { - this.logger.debug('Cached element found, but it is expired'); - - throw new CoreError('Cache entry is expired.'); - } - } - - if (entry.data !== undefined) { - if (!expirationTime) { - this.logger.info(`Cached element found, id: ${id}. Expiration time ignored.`); - } else { - const expires = (expirationTime - now) / 1000; - this.logger.info(`Cached element found, id: ${id}. Expires in expires in ${expires} seconds`); - } - - return { - response: CoreTextUtils.parseJSON(entry.data, {}), - expirationIgnored: forceCache, - expirationTime, - }; - } - - throw new CoreError('Cache entry not valid.'); } /** @@ -1289,65 +263,6 @@ export class CoreSite extends CoreUnauthenticatedSite { ); } - /** - * Save a WS response to cache. - * - * @param method The WebService method. - * @param data Arguments to pass to the method. - * @param response The WS response. - * @param preSets Extra options. - * @returns Promise resolved when the response is saved. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - protected async saveToCache(method: string, data: any, response: any, preSets: CoreSiteWSPreSets): Promise { - if (preSets.uniqueCacheKey) { - // Cache key must be unique, delete all entries with same cache key. - await CoreUtils.ignoreErrors(this.deleteFromCache(method, data, preSets, true)); - } - - // Since 3.7, the expiration time contains the time the entry is modified instead of the expiration time. - // We decided to reuse this field to prevent modifying the database table. - const id = this.getCacheId(method, data); - const entry = { - id, - data: JSON.stringify(response), - expirationTime: Date.now(), - }; - - if (preSets.cacheKey) { - entry['key'] = preSets.cacheKey; - } - - if (preSets.component) { - entry['component'] = preSets.component; - if (preSets.componentId) { - entry['componentId'] = preSets.componentId; - } - } - - await this.cacheTable.insert(entry); - } - - /** - * Delete a WS cache entry or entries. - * - * @param method The WebService method to be called. - * @param data Arguments to pass to the method. - * @param preSets Extra options. - * @param allCacheKey True to delete all entries with the cache key, false to delete only by ID. - * @returns Promise resolved when the entries are deleted. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - protected async deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise { - const id = this.getCacheId(method, data); - - if (allCacheKey) { - await this.cacheTable.delete({ key: preSets.cacheKey }); - } else { - await this.cacheTable.deleteByPrimaryKey({ id }); - } - } - /** * Deletes WS cache entries for all methods relating to a specific component (and * optionally component id). @@ -1404,9 +319,7 @@ export class CoreSite extends CoreUnauthenticatedSite { } /** - * Invalidates all the cache entries. - * - * @returns Promise resolved when the cache entries are invalidated. + * @inheritdoc */ async invalidateWsCache(): Promise { this.logger.debug('Invalidate all the cache for site: ' + this.id); @@ -1419,10 +332,7 @@ export class CoreSite extends CoreUnauthenticatedSite { } /** - * Invalidates all the cache entries with a certain key. - * - * @param key Key to search. - * @returns Promise resolved when the cache entries are invalidated. + * @inheritdoc */ async invalidateWsCacheForKey(key: string): Promise { if (!key) { @@ -1435,28 +345,7 @@ export class CoreSite extends CoreUnauthenticatedSite { } /** - * Invalidates all the cache entries in an array of keys. - * - * @param keys Keys to search. - * @returns Promise resolved when the cache entries are invalidated. - */ - async invalidateMultipleWsCacheForKey(keys: string[]): Promise { - if (!this.db) { - throw new CoreError('Site DB not initialized'); - } - if (!keys || !keys.length) { - return; - } - - this.logger.debug('Invalidating multiple cache keys'); - await Promise.all(keys.map((key) => this.invalidateWsCacheForKey(key))); - } - - /** - * Invalidates all the cache entries whose key starts with a certain value. - * - * @param key Key to search. - * @returns Promise resolved when the cache entries are invalidated. + * @inheritdoc */ async invalidateWsCacheForKeyStartingWith(key: string): Promise { if (!key) { @@ -1563,113 +452,6 @@ export class CoreSite extends CoreUnauthenticatedSite { return space + cache; } - /** - * Returns the URL to the documentation of the app, based on Moodle version and current language. - * - * @param page Docs page to go to. - * @returns Promise resolved with the Moodle docs URL. - */ - getDocsUrl(page?: string): Promise { - const release = this.infos?.release ? this.infos.release : undefined; - - return CoreUrlUtils.getDocsUrl(release, page); - } - - /** - * @inheritdoc - */ - async getPublicConfig(options: { readingStrategy?: CoreSitesReadingStrategy } = {}): Promise { - if (!this.db) { - return super.getPublicConfig(options); - } - - const method = 'tool_mobile_get_public_config'; - const cacheId = this.getCacheId(method, {}); - const cachePreSets: CoreSiteWSPreSets = { - getFromCache: true, - saveToCache: true, - emergencyCache: true, - cacheKey: this.getPublicConfigCacheKey(), - ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), - }; - - if (this.offlineDisabled) { - // Offline is disabled, don't use cache. - cachePreSets.getFromCache = false; - cachePreSets.saveToCache = false; - cachePreSets.emergencyCache = false; - } - - // Check for an ongoing identical request if we're not ignoring cache. - - // Check for an ongoing identical request. - const ongoingRequest = this.getOngoingRequest(cacheId, cachePreSets); - if (ongoingRequest) { - return firstValueFrom(ongoingRequest); - } - - const subject = new Subject(); - const observable = subject.pipe( - // Return a clone of the original object, this may prevent errors if in the callback the object is modified. - map((data) => CoreUtils.clone(data)), - finalize(() => { - this.clearOngoingRequest(cacheId, cachePreSets, observable); - }), - ); - - this.setOngoingRequest(cacheId, cachePreSets, observable); - - this.getFromCache(method, {}, cachePreSets, false) - .then(cachedData => cachedData.response) - .catch(async () => { - if (cachePreSets.forceOffline) { - // Don't call the WS, just fail. - throw new CoreError(Translate.instant('core.cannotconnect')); - } - - // Call the WS. - try { - const config = await this.requestPublicConfig(); - - if (cachePreSets.saveToCache) { - this.saveToCache(method, {}, config, cachePreSets); - } - - return config; - } catch (error) { - cachePreSets.omitExpires = true; - cachePreSets.getFromCache = true; - - try { - const cachedData = await this.getFromCache(method, {}, cachePreSets, true); - - return cachedData.response; - } catch { - throw error; - } - } - }).then((response) => { - // The app doesn't store exceptions for this call, it's safe to assume type CoreSitePublicConfigResponse. - subject.next( response); - subject.complete(); - - return; - }).catch((error) => { - subject.error(error); - }); - - return firstValueFrom(observable); - } - - /** - * Get cache key for getPublicConfig WS calls. - * - * @returns Cache key. - */ - protected getPublicConfigCacheKey(): string { - return 'tool_mobile_get_public_config'; - } - /** * Check if GET method is supported for AJAX calls. * @@ -1928,61 +710,6 @@ export class CoreSite extends CoreUnauthenticatedSite { return this.offlineDisabled; } - /** - * Check if the site version is greater than one or several versions. - * This function accepts a string or an array of strings. If array, the last version must be the highest. - * - * @param versions Version or list of versions to check. - * @returns Whether it's greater or equal, false otherwise. - * @description - * If a string is supplied (e.g. '3.2.1'), it will check if the site version is greater or equal than this version. - * - * If an array of versions is supplied, it will check if the site version is greater or equal than the last version, - * or if it's higher or equal than any of the other releases supplied but lower than the next major release. The last - * version of the array must be the highest version. - * For example, if the values supplied are ['3.0.5', '3.2.3', '3.3.1'] the function will return true if the site version - * is either: - * - Greater or equal than 3.3.1. - * - Greater or equal than 3.2.3 but lower than 3.3. - * - Greater or equal than 3.0.5 but lower than 3.1. - * - * This function only accepts versions from 2.4.0 and above. If any of the versions supplied isn't found, it will assume - * it's the last released major version. - */ - isVersionGreaterEqualThan(versions: string | string[]): boolean { - const info = this.getInfo(); - - if (!info || !info.version) { - return false; - } - - const siteVersion = Number(info.version); - - if (Array.isArray(versions)) { - if (!versions.length) { - return false; - } - - for (let i = 0; i < versions.length; i++) { - const versionNumber = this.getVersionNumber(versions[i]); - if (i == versions.length - 1) { - // It's the last version, check only if site version is greater than this one. - return siteVersion >= versionNumber; - } else { - // Check if site version if bigger than this number but lesser than next major. - if (siteVersion >= versionNumber && siteVersion < this.getNextMajorVersionNumber(versions[i])) { - return true; - } - } - } - } else if (typeof versions == 'string') { - // Compare with this version. - return siteVersion >= this.getVersionNumber(versions); - } - - return false; - } - /** * Given a URL, convert it to a URL that will auto-login if supported. * @@ -2043,78 +770,6 @@ export class CoreSite extends CoreUnauthenticatedSite { } } - /** - * Get a version number from a release version. - * If release version is valid but not found in the list of Moodle releases, it will use the last released major version. - * - * @param version Release version to convert to version number. - * @returns Version number, 0 if invalid. - */ - protected getVersionNumber(version: string): number { - const data = this.getMajorAndMinor(version); - - if (!data) { - // Invalid version. - return 0; - } - - if (CoreSite.MOODLE_RELEASES[data.major] === undefined) { - // Major version not found. Use the last one. - const major = Object.keys(CoreSite.MOODLE_RELEASES).pop(); - if (!major) { - return 0; - } - - data.major = major; - } - - return CoreSite.MOODLE_RELEASES[data.major] + data.minor; - } - - /** - * Given a release version, return the major and minor versions. - * - * @param version Release version (e.g. '3.1.0'). - * @returns Object with major and minor. Returns false if invalid version. - */ - protected getMajorAndMinor(version: string): {major: string; minor: number} | false { - const match = version.match(/^(\d+)(\.(\d+)(\.\d+)?)?/); - if (!match || !match[1]) { - // Invalid version. - return false; - } - - return { - major: match[1] + '.' + (match[3] || '0'), - minor: parseInt(match[5], 10) || 0, - }; - } - - /** - * Given a release version, return the next major version number. - * - * @param version Release version (e.g. '3.1.0'). - * @returns Next major version number. - */ - protected getNextMajorVersionNumber(version: string): number { - const data = this.getMajorAndMinor(version); - const releases = Object.keys(CoreSite.MOODLE_RELEASES); - - if (!data) { - // Invalid version. - return 0; - } - - const position = releases.indexOf(data.major); - - if (position == -1 || position == releases.length - 1) { - // Major version not found or it's the last one. Use the last one. - return CoreSite.MOODLE_RELEASES[releases[position]]; - } - - return CoreSite.MOODLE_RELEASES[releases[position + 1]]; - } - /** * Deletes a site setting. * @@ -2157,24 +812,6 @@ export class CoreSite extends CoreUnauthenticatedSite { await this.configTable.insert({ name, value }); } - /** - * Get a certain cache expiration delay. - * - * @param updateFrequency The update frequency of the entry. - * @returns Expiration delay. - */ - getExpirationDelay(updateFrequency?: number): number { - updateFrequency = updateFrequency || CoreSite.FREQUENCY_USUALLY; - let expirationDelay = this.UPDATE_FREQUENCIES[updateFrequency] || this.UPDATE_FREQUENCIES[CoreSite.FREQUENCY_USUALLY]; - - if (CoreNetwork.isNetworkAccessLimited()) { - // Not WiFi, increase the expiration delay a 50% to decrease the data usage in this case. - expirationDelay *= 1.5; - } - - return expirationDelay; - } - /* * Check if tokenpluginfile script works in the site. * @@ -2286,209 +923,6 @@ export class CoreSite extends CoreUnauthenticatedSite { } -/** - * Operator to chain requests when using observables. - * - * @param readingStrategy Reading strategy used for the current request. - * @param callback Callback called with the result of current request and the reading strategy to use in next requests. - * @returns Operator. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function chainRequests>( - readingStrategy: CoreSitesReadingStrategy | undefined, - callback: (data: T, readingStrategy?: CoreSitesReadingStrategy) => O, -): OperatorFunction> { - return (source: WSObservable) => new Observable<{ data: T; readingStrategy?: CoreSitesReadingStrategy }>(subscriber => { - let firstValue = true; - let isCompleted = false; - - return source.subscribe({ - next: async (value) => { - if (readingStrategy !== CoreSitesReadingStrategy.STALE_WHILE_REVALIDATE) { - // Just use same strategy. - subscriber.next({ data: value, readingStrategy }); - - return; - } - - if (!firstValue) { - // Second (last) value. Chained requests should have used cached data already, just return 1 value now. - subscriber.next({ - data: value, - }); - - return; - } - - firstValue = false; - - // Wait to see if the observable is completed (no more values). - await CoreUtils.nextTick(); - - if (isCompleted) { - // Current request only returns cached data. Let chained requests update in background. - subscriber.next({ data: value, readingStrategy }); - } else { - // Current request will update in background. Prefer cached data in the chained requests. - subscriber.next({ - data: value, - readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE, - }); - } - }, - error: (error) => subscriber.error(error), - complete: async () => { - isCompleted = true; - - await CoreUtils.nextTick(); - - subscriber.complete(); - }, - }); - }).pipe( - mergeMap(({ data, readingStrategy }) => callback(data, readingStrategy)), - ); -} - -/** - * PreSets accepted by the WS call. - */ -export type CoreSiteWSPreSets = { - /** - * Get the value from the cache if it's still valid. - */ - getFromCache?: boolean; - - /** - * Save the result to the cache. - */ - saveToCache?: boolean; - - /** - * Ignore cache expiration. - */ - omitExpires?: boolean; - - /** - * Use the cache when a request fails. Defaults to true. - */ - emergencyCache?: boolean; - - /** - * If true, the app won't call the WS. If the data isn't cached, the call will fail. - */ - forceOffline?: boolean; - - /** - * Extra key to add to the cache when storing this call, to identify the entry. - */ - cacheKey?: string; - - /** - * Whether it should use cache key to retrieve the cached data instead of the request params. - */ - getCacheUsingCacheKey?: boolean; - - /** - * Same as getCacheUsingCacheKey, but for emergency cache. - */ - getEmergencyCacheUsingCacheKey?: boolean; - - /** - * If true, the cache entry will be deleted if the WS call returns an exception. - */ - deleteCacheIfWSError?: boolean; - - /** - * Whether it should only be 1 entry for this cache key (all entries with same key will be deleted). - */ - uniqueCacheKey?: boolean; - - /** - * Whether to filter WS response (moodlewssettingfilter). Defaults to true. - */ - filter?: boolean; - - /** - * Whether to rewrite URLs (moodlewssettingfileurl). Defaults to true. - */ - rewriteurls?: boolean; - - /** - * Language to send to the WebService (moodlewssettinglang). Defaults to app's language. - */ - lang?: string; - - /** - * Defaults to true. Set to false when the expected response is null. - */ - responseExpected?: boolean; - - /** - * Defaults to 'object'. Use it when you expect a type that's not an object|array. - */ - typeExpected?: CoreWSTypeExpected; - - /** - * Wehther a pending request in the queue matching the same function and arguments can be reused instead of adding - * a new request to the queue. Defaults to true for read requests. - */ - reusePending?: boolean; - - /** - * Whether the request will be be sent immediately as a single request. Defaults to false. - */ - skipQueue?: boolean; - - /** - * Cache the response if it returns an errorcode present in this list. - */ - cacheErrors?: string[]; - - /** - * Update frequency. This value determines how often the cached data will be updated. Possible values: - * CoreSite.FREQUENCY_USUALLY, CoreSite.FREQUENCY_OFTEN, CoreSite.FREQUENCY_SOMETIMES, CoreSite.FREQUENCY_RARELY. - * Defaults to CoreSite.FREQUENCY_USUALLY. - */ - updateFrequency?: number; - - /** - * Component name. Optionally included if this request is being made on behalf of a specific - * component (e.g. activity). - */ - component?: string; - - /** - * Component id. Optionally included when 'component' is set. - */ - componentId?: number; - - /** - * Whether to split a request if it has too many parameters. Sending too many parameters to the site - * can cause the request to fail (see PHP's max_input_vars). - */ - splitRequest?: CoreWSPreSetsSplitRequest; - - /** - * If true, the app will return cached data even if it's expired and then it'll call the WS in the background. - * Only enabled if CoreConstants.CONFIG.disableCallWSInBackground isn't true. - */ - updateInBackground?: boolean; -}; - -/** - * Info of a request waiting in the queue. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type RequestQueueItem = { - cacheId: string; - method: string; - data: any; // eslint-disable-line @typescript-eslint/no-explicit-any - preSets: CoreSiteWSPreSets; - wsPreSets: CoreWSPreSets; - deferred: CorePromisedValue; -}; - /** * Result of WS tool_mobile_get_config. */ @@ -2518,17 +952,6 @@ export type CoreSiteAutologinKeyResult = { warnings?: CoreWSExternalWarning[]; }; -/** - * Result of WS tool_mobile_call_external_functions. - */ -export type CoreSiteCallExternalFunctionsResult = { - responses: { - error: boolean; // Whether an exception was thrown. - data?: string; // JSON-encoded response data. - exception?: string; // JSON-encoed exception info. - }[]; -}; - /** * Options for storeLastViewed. */ @@ -2536,36 +959,3 @@ export type CoreSiteStoreLastViewedOptions = { data?: string; // Other data. timeaccess?: number; // Accessed time. If not set, current time. }; - -/** - * Info about cached data. - */ -type WSCachedData = { - response: T | WSCachedError; // The WS response data, or an error if the WS returned an error and it was cached. - expirationIgnored: boolean; // Whether the expiration time was ignored. - expirationTime?: number; // Entry expiration time (only if not ignored). -}; - -/** - * Error data stored in cache. - */ -type WSCachedError = { - exception?: string; - errorcode?: string; -}; - -/** - * Observable returned when calling WebServices. - * If the request uses the "update in background" feature, it will return 2 values: first the cached one, and then the one - * coming from the server. After this, it will complete. - * Otherwise, it will only return 1 value, either coming from cache or from the server. After this, it will complete. - */ -export type WSObservable = Observable; - -/** - * Type of ongoing requests stored in memory to avoid duplicating them. - */ -enum OngoingRequestType { - STANDARD = 0, - UPDATE_IN_BACKGROUND = 1, -} diff --git a/src/core/features/comments/services/comments.ts b/src/core/features/comments/services/comments.ts index 2e265f58b..6359fb3c9 100644 --- a/src/core/features/comments/services/comments.ts +++ b/src/core/features/comments/services/comments.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; @@ -23,6 +23,7 @@ import { makeSingleton } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { CoreCommentsOffline } from './comments-offline'; import { CoreCommentsSyncAutoSyncData, CoreCommentsSyncProvider } from './comments-sync'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmComments:'; diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 066f9e1bf..05f832cda 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -21,7 +21,7 @@ import { CoreLogger } from '@singletons/logger'; import { CoreSitesCommonWSOptions, CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; -import { CoreSiteWSPreSets, CoreSite, WSObservable } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreConstants } from '@/core/constants'; import { makeSingleton, Translate } from '@singletons'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; @@ -55,6 +55,7 @@ import { SQLiteDB } from '@classes/sqlitedb'; import { CorePlatform } from '@services/platform'; import { asyncObservable, firstValueFrom } from '@/core/utils/rxjs'; import { map } from 'rxjs/operators'; +import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmCourse:'; diff --git a/src/core/features/course/services/module-prefetch-delegate.ts b/src/core/features/course/services/module-prefetch-delegate.ts index bed817209..bd0d84f55 100644 --- a/src/core/features/course/services/module-prefetch-delegate.ts +++ b/src/core/features/course/services/module-prefetch-delegate.ts @@ -24,7 +24,7 @@ import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { CoreCourse, CoreCourseAnyModuleData, CoreCourseModuleContentFile } from './course'; import { CoreCache } from '@classes/cache'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; import { CoreConstants } from '@/core/constants'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { makeSingleton } from '@singletons'; diff --git a/src/core/features/courses/services/courses-helper.ts b/src/core/features/courses/services/courses-helper.ts index 20c5b3b66..9bf1e2c1d 100644 --- a/src/core/features/courses/services/courses-helper.ts +++ b/src/core/features/courses/services/courses-helper.ts @@ -29,7 +29,7 @@ import moment from 'moment-timezone'; import { of } from 'rxjs'; import { firstValueFrom, zipIncludingComplete } from '@/core/utils/rxjs'; import { catchError, map } from 'rxjs/operators'; -import { chainRequests, WSObservable } from '@classes/sites/site'; +import { chainRequests, WSObservable } from '@classes/sites/candidate-site'; // Id for a course item representing all courses (for example, for course filters). export const ALL_COURSES_ID = -1; diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index 26ad36153..34f7b2df2 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; -import { CoreSite, CoreSiteWSPreSets, WSObservable } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { makeSingleton } from '@singletons'; import { CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { CoreEvents } from '@singletons/events'; @@ -25,6 +25,7 @@ import { map } from 'rxjs/operators'; import { AddonEnrolGuest, AddonEnrolGuestInfo } from '@addons/enrol/guest/services/guest'; import { AddonEnrolSelf } from '@addons/enrol/self/services/self'; import { CoreEnrol, CoreEnrolEnrolmentInfo, CoreEnrolEnrolmentMethod } from '@features/enrol/services/enrol'; +import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmCourses:'; diff --git a/src/core/features/courses/services/dashboard.ts b/src/core/features/courses/services/dashboard.ts index e72ad5ba5..a8fd4c57a 100644 --- a/src/core/features/courses/services/dashboard.ts +++ b/src/core/features/courses/services/dashboard.ts @@ -14,13 +14,14 @@ import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; -import { CoreSite, CoreSiteWSPreSets, WSObservable } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseBlock } from '@features/course/services/course'; import { CoreStatusWithWarningsWSResponse } from '@services/ws'; import { makeSingleton } from '@singletons'; import { CoreError } from '@classes/errors/error'; import { map } from 'rxjs/operators'; import { asyncObservable, firstValueFrom } from '@/core/utils/rxjs'; +import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'CoreCoursesDashboard:'; diff --git a/src/core/features/filter/services/filter.ts b/src/core/features/filter/services/filter.ts index cc9d45e2a..b7ff84a23 100644 --- a/src/core/features/filter/services/filter.ts +++ b/src/core/features/filter/services/filter.ts @@ -16,13 +16,14 @@ import { Injectable } from '@angular/core'; import { CoreNetwork } from '@services/network'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreTextUtils } from '@services/utils/text'; import { CoreFilterDelegate } from './filter-delegate'; import { makeSingleton } from '@singletons'; import { CoreEvents, CoreEventSiteData } from '@singletons/events'; import { CoreLogger } from '@singletons/logger'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; /** * Service to provide filter functionalities. diff --git a/src/core/features/grades/services/grades.ts b/src/core/features/grades/services/grades.ts index 7aa10b24a..cd72f3d28 100644 --- a/src/core/features/grades/services/grades.ts +++ b/src/core/features/grades/services/grades.ts @@ -18,7 +18,7 @@ import { CoreSites } from '@services/sites'; import { makeSingleton } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreWSExternalWarning } from '@services/ws'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; import { CoreError } from '@classes/errors/error'; import { SafeNumber } from '@/core/utils/types'; diff --git a/src/core/features/h5p/services/h5p.ts b/src/core/features/h5p/services/h5p.ts index 6742c1f26..d12a1eb64 100644 --- a/src/core/features/h5p/services/h5p.ts +++ b/src/core/features/h5p/services/h5p.ts @@ -18,7 +18,7 @@ import { CoreSites } from '@services/sites'; import { CoreWSExternalWarning, CoreWSExternalFile, CoreWSFile } from '@services/ws'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreQueueRunner } from '@classes/queue-runner'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreH5PCore } from '../classes/core'; import { CoreH5PFramework } from '../classes/framework'; @@ -29,6 +29,7 @@ import { CoreH5PValidator } from '../classes/validator'; import { makeSingleton } from '@singletons'; import { CoreError } from '@classes/errors/error'; import { CorePath } from '@singletons/path'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; /** * Service to provide H5P functionalities. diff --git a/src/core/features/rating/services/rating.ts b/src/core/features/rating/services/rating.ts index 54441df2b..bdfd3846d 100644 --- a/src/core/features/rating/services/rating.ts +++ b/src/core/features/rating/services/rating.ts @@ -14,7 +14,7 @@ import { ContextLevel } from '@/core/constants'; import { Injectable } from '@angular/core'; -import { CoreSiteWSPreSets, CoreSite } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreUser } from '@features/user/services/user'; import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; @@ -23,6 +23,7 @@ import { CoreWSExternalWarning } from '@services/ws'; import { makeSingleton } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { CoreRatingOffline } from './rating-offline'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'CoreRating:'; diff --git a/src/core/features/reportbuilder/services/reportbuilder.ts b/src/core/features/reportbuilder/services/reportbuilder.ts index d1cba2e5a..0d06aaa6c 100644 --- a/src/core/features/reportbuilder/services/reportbuilder.ts +++ b/src/core/features/reportbuilder/services/reportbuilder.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; import { CoreSites } from '@services/sites'; import { CoreWSExternalWarning } from '@services/ws'; import { makeSingleton } from '@singletons'; diff --git a/src/core/features/sitehome/services/sitehome.ts b/src/core/features/sitehome/services/sitehome.ts index bc83d96b5..c45908f3a 100644 --- a/src/core/features/sitehome/services/sitehome.ts +++ b/src/core/features/sitehome/services/sitehome.ts @@ -15,13 +15,14 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { makeSingleton } from '@singletons'; import { CoreCourse } from '../../course/services/course'; import { CoreCourses } from '../../courses/services/courses'; import { AddonModForum, AddonModForumData } from '@addons/mod/forum/services/forum'; import { CoreError } from '@classes/errors/error'; import { CoreBlockHelper } from '@features/block/services/block-helper'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; /** * Items with index 1 and 3 were removed on 2.5 and not being supported in the app. diff --git a/src/core/features/sitehome/tests/links.test.ts b/src/core/features/sitehome/tests/links.test.ts index 513a2966c..f137e69ec 100644 --- a/src/core/features/sitehome/tests/links.test.ts +++ b/src/core/features/sitehome/tests/links.test.ts @@ -30,7 +30,7 @@ describe('Site Home link handlers', () => { mockSingleton(CoreSites, mock({ isStoredRootURL: () => Promise.resolve({ siteIds: [siteId] }), - getSite: () => Promise.resolve(new CoreSite(siteId, siteUrl)), + getSite: () => Promise.resolve(new CoreSite(siteId, siteUrl, '')), getSiteIdsFromUrl: () => Promise.resolve([siteId]), })); diff --git a/src/core/features/siteplugins/classes/call-ws-directive.ts b/src/core/features/siteplugins/classes/call-ws-directive.ts index 85348c699..3d2287c7d 100644 --- a/src/core/features/siteplugins/classes/call-ws-directive.ts +++ b/src/core/features/siteplugins/classes/call-ws-directive.ts @@ -15,7 +15,7 @@ import { Input, OnInit, OnDestroy, ElementRef, Output, EventEmitter, Directive } from '@angular/core'; import { Subscription } from 'rxjs'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; import { CoreSitePlugins } from '../services/siteplugins'; import { CoreLogger } from '@singletons/logger'; diff --git a/src/core/features/siteplugins/components/module-index/module-index.ts b/src/core/features/siteplugins/components/module-index/module-index.ts index 5fe9c342a..e13a4dd21 100644 --- a/src/core/features/siteplugins/components/module-index/module-index.ts +++ b/src/core/features/siteplugins/components/module-index/module-index.ts @@ -14,7 +14,7 @@ import { Component, OnInit, OnDestroy, Input, ViewChild, HostBinding } from '@angular/core'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; import { CoreCourseModuleSummaryResult, CoreCourseModuleSummaryComponent, diff --git a/src/core/features/siteplugins/components/plugin-content/plugin-content.ts b/src/core/features/siteplugins/components/plugin-content/plugin-content.ts index c132c3e12..e13667e2c 100644 --- a/src/core/features/siteplugins/components/plugin-content/plugin-content.ts +++ b/src/core/features/siteplugins/components/plugin-content/plugin-content.ts @@ -27,7 +27,7 @@ import { import { Subject } from 'rxjs'; import { Md5 } from 'ts-md5'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; import { CoreCompileHtmlComponent } from '@features/compile/components/compile-html/compile-html'; import { CoreSitePlugins, CoreSitePluginsContent, CoreSitePluginsProvider } from '@features/siteplugins/services/siteplugins'; import { CoreNavigator } from '@services/navigator'; diff --git a/src/core/features/siteplugins/directives/call-ws-new-content.ts b/src/core/features/siteplugins/directives/call-ws-new-content.ts index 8e4e836a5..b52731946 100644 --- a/src/core/features/siteplugins/directives/call-ws-new-content.ts +++ b/src/core/features/siteplugins/directives/call-ws-new-content.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Directive, Input, ElementRef, Optional } from '@angular/core'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; import { CoreNavigator } from '@services/navigator'; import { CoreUtils } from '@services/utils/utils'; import { Md5 } from 'ts-md5'; diff --git a/src/core/features/siteplugins/directives/new-content.ts b/src/core/features/siteplugins/directives/new-content.ts index ffa1afa29..8bf592928 100644 --- a/src/core/features/siteplugins/directives/new-content.ts +++ b/src/core/features/siteplugins/directives/new-content.ts @@ -15,7 +15,7 @@ import { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; import { Md5 } from 'ts-md5'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; import { CoreNavigator } from '@services/navigator'; import { CoreUtils } from '@services/utils/utils'; import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; diff --git a/src/core/features/siteplugins/pages/plugin/plugin.ts b/src/core/features/siteplugins/pages/plugin/plugin.ts index d2b0fca98..6532a7bd1 100644 --- a/src/core/features/siteplugins/pages/plugin/plugin.ts +++ b/src/core/features/siteplugins/pages/plugin/plugin.ts @@ -14,7 +14,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; import { CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; import { CanLeave } from '@guards/can-leave'; import { CoreNavigator } from '@services/navigator'; diff --git a/src/core/features/siteplugins/services/siteplugins-helper.ts b/src/core/features/siteplugins/services/siteplugins-helper.ts index a1514bbf7..ffcec0afb 100644 --- a/src/core/features/siteplugins/services/siteplugins-helper.ts +++ b/src/core/features/siteplugins/services/siteplugins-helper.ts @@ -20,7 +20,7 @@ import { AddonModAssignSubmissionDelegate } from '@addons/mod/assign/services/su import { AddonModQuizAccessRuleDelegate } from '@addons/mod/quiz/services/access-rules-delegate'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreError } from '@classes/errors/error'; -import { CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; import { CoreBlockDelegate } from '@features/block/services/block-delegate'; import { CoreCompile } from '@features/compile/services/compile'; import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; diff --git a/src/core/features/siteplugins/services/siteplugins.ts b/src/core/features/siteplugins/services/siteplugins.ts index 4a133b1ff..56e70fab2 100644 --- a/src/core/features/siteplugins/services/siteplugins.ts +++ b/src/core/features/siteplugins/services/siteplugins.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreConstants } from '@/core/constants'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreCourseAnyModuleData } from '@features/course/services/course'; import { CoreCourses } from '@features/courses/services/courses'; import { CoreApp } from '@services/app'; @@ -32,6 +32,7 @@ import { CoreSitePluginsModuleHandler } from '../classes/handlers/module-handler import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; import { CoreEnrolAction, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'CoreSitePlugins:'; diff --git a/src/core/features/tag/services/tag.ts b/src/core/features/tag/services/tag.ts index 25d0660cd..bf5f6ec0c 100644 --- a/src/core/features/tag/services/tag.ts +++ b/src/core/features/tag/services/tag.ts @@ -14,10 +14,11 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { CoreError } from '@classes/errors/error'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'CoreTag:'; diff --git a/src/core/features/user/services/user.ts b/src/core/features/user/services/user.ts index eb62ff86c..b40dcb60a 100644 --- a/src/core/features/user/services/user.ts +++ b/src/core/features/user/services/user.ts @@ -20,7 +20,7 @@ import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { CoreUserOffline } from './user-offline'; import { CoreLogger } from '@singletons/logger'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { makeSingleton, Translate } from '@singletons'; import { CoreEvents, CoreEventSiteData, CoreEventUserDeletedData, CoreEventUserSuspendedData } from '@singletons/events'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; @@ -28,6 +28,7 @@ import { CoreError } from '@classes/errors/error'; import { USERS_TABLE_NAME, CoreUserDBRecord } from './database/user'; import { CoreUserHelper } from './user-helper'; import { CoreUrlUtils } from '@services/utils/url'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmUser:'; diff --git a/src/core/features/xapi/services/xapi.ts b/src/core/features/xapi/services/xapi.ts index 2171c85df..4562e804d 100644 --- a/src/core/features/xapi/services/xapi.ts +++ b/src/core/features/xapi/services/xapi.ts @@ -17,13 +17,14 @@ import { Injectable } from '@angular/core'; import { CoreNetwork } from '@services/network'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreXAPIOffline, CoreXAPIOfflineSaveStatementsOptions } from './offline'; import { makeSingleton } from '@singletons'; import { CoreXAPIItemAgent } from '../classes/item-agent'; import { CoreXAPIIRI } from '../classes/iri'; import { CoreError } from '@classes/errors/error'; import { CoreLogger } from '@singletons/logger'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; export const XAPI_STATE_DELETED = 'STATE_DELETED'; diff --git a/src/core/services/groups.ts b/src/core/services/groups.ts index 1831cdc64..6fa1584c5 100644 --- a/src/core/services/groups.ts +++ b/src/core/services/groups.ts @@ -15,11 +15,12 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/sites/site'; +import { CoreSite } from '@classes/sites/site'; import { CoreError } from '@classes/errors/error'; import { makeSingleton, Translate } from '@singletons'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreCourses } from '@features/courses/services/courses'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; const ROOT_CACHE_KEY = 'mmGroups:'; diff --git a/src/core/services/sites-factory.ts b/src/core/services/sites-factory.ts index 765d34df6..fa07cba03 100644 --- a/src/core/services/sites-factory.ts +++ b/src/core/services/sites-factory.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreCandidateSite } from '@classes/sites/candidate-site'; import { CoreSite, CoreSiteConfig } from '@classes/sites/site'; import { CoreUnauthenticatedSite, CoreSiteInfo } from '@classes/sites/unauthenticated-site'; @@ -37,9 +38,9 @@ export class CoreSitesFactoryService { * @returns Site instance. */ makeSite( - id: string | undefined, + id: string, siteUrl: string, - token?: string, + token: string, info?: CoreSiteInfo, privateToken?: string, config?: CoreSiteConfig, @@ -48,6 +49,18 @@ export class CoreSitesFactoryService { return new CoreSite(id, siteUrl, token, info, privateToken, config, loggedOut); } + /** + * Create a candidate site instance. + * + * @param siteUrl Site URL. + * @param token Site's WS token. + * @param privateToken Private token. + * @returns Candidate site instance. + */ + makeCandidateSite(siteUrl: string, token: string, privateToken?: string): CoreCandidateSite { + return new CoreCandidateSite(siteUrl, token, privateToken); + } + /** * Create an unauthenticated site instance. * diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index fecc642a8..f1c60519a 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -26,7 +26,6 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { CoreSite, - CoreSiteWSPreSets, CoreSiteConfig, } from '@classes/sites/site'; import { SQLiteDB, SQLiteDBRecordValues, SQLiteDBTableSchema } from '@classes/sqlitedb'; @@ -65,6 +64,7 @@ import { CoreContentLinksHelper } from '@features/contentlinks/services/contentl import { CoreAutoLogoutType, CoreAutoLogout } from '@features/autologout/services/autologout'; import { CoreCacheManager } from '@services/cache-manager'; import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse } from '@classes/sites/unauthenticated-site'; +import { CoreSiteWSPreSets } from '@classes/sites/candidate-site'; export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id'; @@ -515,7 +515,7 @@ export class CoreSitesProvider { } // Create a "candidate" site to fetch the site info. - let candidateSite = CoreSitesFactory.makeSite(undefined, siteUrl, token, undefined, privateToken); + const candidateSite = CoreSitesFactory.makeCandidateSite(siteUrl, token, privateToken); let isNewSite = true; try { @@ -529,33 +529,33 @@ export class CoreSitesProvider { const siteId = this.createSiteID(info.siteurl, info.username); // Check if the site already exists. - const site = await CoreUtils.ignoreErrors(this.getSite(siteId)); + const storedSite = await CoreUtils.ignoreErrors(this.getSite(siteId)); + let site: CoreSite; - if (site) { - // Site already exists, update its data and use it. + if (storedSite) { + // Site already exists. isNewSite = false; - candidateSite = site; - candidateSite.setToken(token); - candidateSite.setPrivateToken(privateToken); - candidateSite.setInfo(info); - candidateSite.setOAuthId(oauthId); - candidateSite.setLoggedOut(false); + site = storedSite; + site.setToken(token); + site.setPrivateToken(privateToken); + site.setInfo(info); + site.setOAuthId(oauthId); + site.setLoggedOut(false); } else { // New site, set site ID and info. isNewSite = true; - candidateSite.setId(siteId); - candidateSite.setInfo(info); - candidateSite.setOAuthId(oauthId); + site = CoreSitesFactory.makeSite(siteId, siteUrl, token, info, privateToken); + site.setOAuthId(oauthId); // Create database tables before login and before any WS call. - await this.migrateSiteSchemas(candidateSite); + await this.migrateSiteSchemas(site); } // Try to get the site config. let config: CoreSiteConfig | undefined; try { - config = await this.getSiteConfig(candidateSite); + config = await this.getSiteConfig(site); } catch (error) { // Ignore errors if it's not a new site, we'll use the config already stored. if (isNewSite) { @@ -564,16 +564,16 @@ export class CoreSitesProvider { } if (config !== undefined) { - candidateSite.setConfig(config); + site.setConfig(config); } // Add site to sites list. await this.addSite(siteId, siteUrl, token, info, privateToken, config, oauthId); - this.sites[siteId] = candidateSite; + this.sites[siteId] = site; if (login) { // Turn candidate site into current site. - this.currentSite = candidateSite; + this.currentSite = site; // Store session. await this.login(siteId); } else if (this.currentSite && this.currentSite.getId() == siteId) { @@ -2065,7 +2065,7 @@ export class CoreSitesProvider { await Promise.all( sites - .map(site => CoreSitesFactory.makeSite(site.id, site.siteUrl)) + .map(site => CoreSitesFactory.makeSite(site.id, site.siteUrl, '')) .map(site => site.invalidateCaches()), ); } diff --git a/src/storybook/stubs/classes/site.ts b/src/storybook/stubs/classes/site.ts index b21071a61..59a6fb7ff 100644 --- a/src/storybook/stubs/classes/site.ts +++ b/src/storybook/stubs/classes/site.ts @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreSite, CoreSiteConfigResponse, CoreSiteWSPreSets, WSObservable } from '@classes/sites/site'; +import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/candidate-site'; +import { CoreSite, CoreSiteConfigResponse } from '@classes/sites/site'; import { CoreSiteInfo } from '@classes/sites/unauthenticated-site'; import { of } from 'rxjs'; @@ -26,7 +27,7 @@ export class CoreSiteStub extends CoreSite { protected wsStubs: Record = {}; constructor (fixture: CoreSiteFixture) { - super(fixture.id, fixture.info.siteurl, undefined, fixture.info); + super(fixture.id, fixture.info.siteurl, '', fixture.info); this.stubWSResponse('tool_mobile_get_config', { settings: [],