diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index a176115c2..f44b2ae89 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -69,8 +69,7 @@ jobs: cat circular-dependencies lines=$(cat circular-dependencies | wc -l) echo "Total circular dependencies: $lines" - test $lines -ge 138 - test $lines -le 148 + test $lines -eq 135 - name: JavaScript code compatibility run: | npx check-es-compat www/*.js --polyfills="\{Array,String,TypedArray\}.prototype.at,Object.hasOwn" diff --git a/local_moodleappbehat/tests/behat/behat_app_helper.php b/local_moodleappbehat/tests/behat/behat_app_helper.php index 6cf42e881..45dc62741 100644 --- a/local_moodleappbehat/tests/behat/behat_app_helper.php +++ b/local_moodleappbehat/tests/behat/behat_app_helper.php @@ -454,7 +454,7 @@ class behat_app_helper extends behat_base { $result = $this->zone_js("customUrlSchemes.handleCustomURL('$customurl')"); if ($result !== 'OK') { - throw new DriverException('Error handling url - ' . $result); + throw new DriverException('Error handling url - ' . $customurl . ' - '.$result); } if (!empty($successXPath)) { // Wait until the page appears. diff --git a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts index 6bd07f093..a8c52eb53 100644 --- a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts +++ b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts @@ -22,7 +22,7 @@ import { Translate } from '@singletons'; import { CoreUtils } from '@services/utils/utils'; import { CoreNavigator } from '@services/navigator'; import { CoreCourseHelper } from '@features/course/services/course-helper'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreSharedModule } from '@/core/shared.module'; /** @@ -103,7 +103,7 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i brandedIcons[mod.modname] = mod.branded; // If this is not a theme image, leave it undefined to avoid having specific activity icons. - if (CoreUrlUtils.isThemeImageUrl(mod.modicon)) { + if (CoreUrl.isThemeImageUrl(mod.modicon)) { modIcons[mod.modname] = mod.modicon; } }); diff --git a/src/addons/blog/pages/index/index.ts b/src/addons/blog/pages/index/index.ts index 63e08dd3f..430e425b6 100644 --- a/src/addons/blog/pages/index/index.ts +++ b/src/addons/blog/pages/index/index.ts @@ -26,7 +26,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreArray } from '@singletons/array'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -79,7 +79,7 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy { ...this.filter, category: 'blog', }, - url: CoreUrlUtils.addParamsToUrl('/blog/index.php', { + url: CoreUrl.addParamsToUrl('/blog/index.php', { ...this.filter, modid: this.filter.cmid, cmid: undefined, diff --git a/src/addons/calendar/components/calendar/calendar.ts b/src/addons/calendar/components/calendar/calendar.ts index 6fac088f5..e2df77771 100644 --- a/src/addons/calendar/components/calendar/calendar.ts +++ b/src/addons/calendar/components/calendar/calendar.ts @@ -50,7 +50,7 @@ import { import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/swipe-slides-dynamic-items-manager'; import moment from 'moment-timezone'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreTime } from '@singletons/time'; import { Translate } from '@singletons'; @@ -132,7 +132,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro ...params, category: 'calendar', }, - url: CoreUrlUtils.addParamsToUrl('/calendar/view.php?view=month', params), + url: CoreUrl.addParamsToUrl('/calendar/view.php?view=month', params), }); }); } diff --git a/src/addons/calendar/components/upcoming-events/upcoming-events.ts b/src/addons/calendar/components/upcoming-events/upcoming-events.ts index 862712285..753a38b55 100644 --- a/src/addons/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addons/calendar/components/upcoming-events/upcoming-events.ts @@ -26,7 +26,7 @@ import { AddonCalendarOffline } from '../../services/calendar-offline'; import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses'; import { CoreConstants } from '@/core/constants'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreTime } from '@singletons/time'; import { Translate } from '@singletons'; @@ -103,7 +103,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On ...params, category: 'calendar', }, - url: CoreUrlUtils.addParamsToUrl('/calendar/view.php?view=upcoming', params), + url: CoreUrl.addParamsToUrl('/calendar/view.php?view=upcoming', params), }); }); } diff --git a/src/addons/calendar/pages/day/day.ts b/src/addons/calendar/pages/day/day.ts index 2a57565f9..1edf50164 100644 --- a/src/addons/calendar/pages/day/day.ts +++ b/src/addons/calendar/pages/day/day.ts @@ -47,7 +47,7 @@ import { import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { AddonCalendarEventsSource } from '@addons/calendar/classes/events-source'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreTime } from '@singletons/time'; /** @@ -201,7 +201,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { ...params, category: 'calendar', }, - url: CoreUrlUtils.addParamsToUrl('/calendar/view.php?view=day', params), + url: CoreUrl.addParamsToUrl('/calendar/view.php?view=day', params), }); }); } diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index db0bc8437..71817798b 100644 --- a/src/addons/calendar/services/calendar.ts +++ b/src/addons/calendar/services/calendar.ts @@ -18,7 +18,7 @@ import { CoreSite } from '@classes/sites/site'; import { CoreNetwork } from '@services/network'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreGroups } from '@services/groups'; import { CoreLocalNotifications } from '@services/local-notifications'; @@ -362,14 +362,14 @@ export class AddonCalendarProvider { // Add links to the days if needed. if (dayStart && (!seenDay || !moment(seenDay).isSame(start, 'day'))) { promises.push(this.getViewUrl('day', event.timestart, undefined, siteId).then((url) => { - dayStart = CoreUrlUtils.buildLink(url, dayStart); + dayStart = CoreUrl.buildLink(url, dayStart); return; })); } if (dayEnd && (!seenDay || !moment(seenDay).isSame(end, 'day'))) { promises.push(this.getViewUrl('day', end / 1000, undefined, siteId).then((url) => { - dayEnd = CoreUrlUtils.buildLink(url, dayEnd); + dayEnd = CoreUrl.buildLink(url, dayEnd); return; })); @@ -398,7 +398,7 @@ export class AddonCalendarProvider { // Add link to view the day. const url = await this.getViewUrl('day', event.timestart, undefined, siteId); - return CoreUrlUtils.buildLink(url, this.getDayRepresentation(start, useCommonWords)) + ', ' + time; + return CoreUrl.buildLink(url, this.getDayRepresentation(start, useCommonWords)) + ', ' + time; } /** diff --git a/src/addons/competency/pages/competency/competency.page.ts b/src/addons/competency/pages/competency/competency.page.ts index 615a146c2..cb94fde05 100644 --- a/src/addons/competency/pages/competency/competency.page.ts +++ b/src/addons/competency/pages/competency/competency.page.ts @@ -40,7 +40,7 @@ import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; import { CoreTime } from '@singletons/time'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; /** * Page that displays the competency information. @@ -306,7 +306,7 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy { planstatus: this.planStatus, userid: userId, }, - url: CoreUrlUtils.addParamsToUrl('/admin/tool/lp/user_competency_in_plan.php', { + url: CoreUrl.addParamsToUrl('/admin/tool/lp/user_competency_in_plan.php', { planid: source.PLAN_ID, userid: userId, competencyid: compId, @@ -328,7 +328,7 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy { courseid: source.COURSE_ID, userid: userId, }, - url: CoreUrlUtils.addParamsToUrl('/admin/tool/lp/user_competency_in_course.php', { + url: CoreUrl.addParamsToUrl('/admin/tool/lp/user_competency_in_course.php', { courseid: source.COURSE_ID, competencyid: compId, userid: userId, diff --git a/src/addons/competency/services/handlers/push-click.ts b/src/addons/competency/services/handlers/push-click.ts index b28423ebf..63e4b1a50 100644 --- a/src/addons/competency/services/handlers/push-click.ts +++ b/src/addons/competency/services/handlers/push-click.ts @@ -18,7 +18,7 @@ import { COURSE_PAGE_NAME } from '@features/course/constants'; import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; import { CoreNavigator } from '@services/navigator'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonCompetency } from '../competency'; @@ -49,7 +49,7 @@ export class AddonCompetencyPushClickHandlerService implements CorePushNotificat * @inheritdoc */ async handleClick(notification: AddonCompetencyPushNotificationData): Promise { - const contextUrlParams = CoreUrlUtils.extractUrlParams(notification.contexturl); + const contextUrlParams = CoreUrl.extractUrlParams(notification.contexturl); if (notification.name == 'competencyplancomment') { // Open the learning plan. diff --git a/src/addons/filter/displayh5p/services/handlers/displayh5p.ts b/src/addons/filter/displayh5p/services/handlers/displayh5p.ts index 3949f2b15..e71a429fa 100644 --- a/src/addons/filter/displayh5p/services/handlers/displayh5p.ts +++ b/src/addons/filter/displayh5p/services/handlers/displayh5p.ts @@ -18,7 +18,7 @@ import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/def import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter'; import { makeSingleton } from '@singletons'; import { CoreH5PPlayerComponent } from '@features/h5p/components/h5p-player/h5p-player'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreH5PHelper } from '@features/h5p/classes/helper'; /** @@ -57,7 +57,7 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle embeddedH5PIframes.forEach((iframe) => { // Add the preventredirect param to allow authenticating if auto-login fails. - iframe.src = CoreUrlUtils.addParamsToUrl(iframe.src, { preventredirect: false }); + iframe.src = CoreUrl.addParamsToUrl(iframe.src, { preventredirect: false }); // Add resizer script so the H5P has the right height. CoreH5PHelper.addResizerScript(); diff --git a/src/addons/filter/mediaplugin/services/videojs.ts b/src/addons/filter/mediaplugin/services/videojs.ts index 2b5ea07e7..a51932900 100644 --- a/src/addons/filter/mediaplugin/services/videojs.ts +++ b/src/addons/filter/mediaplugin/services/videojs.ts @@ -17,7 +17,7 @@ import { CorePromisedValue } from '@classes/promised-value'; import { CoreExternalContentDirective } from '@directives/external-content'; import { CoreLang } from '@services/lang'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { makeSingleton } from '@singletons'; import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreEvents } from '@singletons/events'; @@ -107,7 +107,7 @@ export class AddonFilterMediaPluginVideoJSService { const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}'; const data = CoreTextUtils.parseJSON(dataSetupString, {}); - const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrlUtils.getYoutubeEmbedUrl(data.sources?.[0]?.src); + const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrl.getYoutubeEmbedUrl(data.sources?.[0]?.src); if (!youtubeUrl) { return; diff --git a/src/addons/mod/assign/services/handlers/push-click.ts b/src/addons/mod/assign/services/handlers/push-click.ts index af0d797e9..9388eb4d2 100644 --- a/src/addons/mod/assign/services/handlers/push-click.ts +++ b/src/addons/mod/assign/services/handlers/push-click.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonModAssign } from '../assign'; @@ -50,7 +50,7 @@ export class AddonModAssignPushClickHandlerService implements CorePushNotificati * @returns Promise resolved when done. */ async handleClick(notification: NotificationData): Promise { - const contextUrlParams = CoreUrlUtils.extractUrlParams(notification.contexturl); + const contextUrlParams = CoreUrl.extractUrlParams(notification.contexturl); const courseId = Number(notification.courseid); const moduleId = Number(contextUrlParams.id); diff --git a/src/addons/mod/book/pages/contents/contents.ts b/src/addons/mod/book/pages/contents/contents.ts index 653974be2..77b1f47ac 100644 --- a/src/addons/mod/book/pages/contents/contents.ts +++ b/src/addons/mod/book/pages/contents/contents.ts @@ -38,7 +38,7 @@ import { AddonModBookTocChapter, } from '../../services/book'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { ADDON_MOD_BOOK_COMPONENT, AddonModBookNavStyle } from '../../constants'; /** @@ -293,7 +293,7 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy { ws: 'mod_book_view_book', name: this.module.name, data: { id: this.module.instance, category: 'book', chapterid: chapterId }, - url: CoreUrlUtils.addParamsToUrl(`/mod/book/view.php?id=${this.module.id}`, { chapterid: chapterId }), + url: CoreUrl.addParamsToUrl(`/mod/book/view.php?id=${this.module.id}`, { chapterid: chapterId }), }); const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId); diff --git a/src/addons/mod/book/services/handlers/tag-area.ts b/src/addons/mod/book/services/handlers/tag-area.ts index fd6da7c45..5bcc9c054 100644 --- a/src/addons/mod/book/services/handlers/tag-area.ts +++ b/src/addons/mod/book/services/handlers/tag-area.ts @@ -18,7 +18,7 @@ import { CoreTagFeedComponent } from '@features/tag/components/feed/feed'; import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate'; import { CoreTagFeedElement, CoreTagHelper } from '@features/tag/services/tag-helper'; import { CoreSitesReadingStrategy } from '@services/sites'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { makeSingleton } from '@singletons'; import { AddonModBook } from '../book'; @@ -51,7 +51,7 @@ export class AddonModBookTagAreaHandlerService implements CoreTagAreaHandler { // Find module ids of the returned books, they are needed by the link delegate. await Promise.all(items.map(async (item) => { - const params = item.url ? CoreUrlUtils.extractUrlParams(item.url) : {}; + const params = item.url ? CoreUrl.extractUrlParams(item.url) : {}; if (params.b && !params.id) { const bookId = parseInt(params.b, 10); diff --git a/src/addons/mod/data/components/index/index.ts b/src/addons/mod/data/components/index/index.ts index aaf630e75..c8c029718 100644 --- a/src/addons/mod/data/components/index/index.ts +++ b/src/addons/mod/data/components/index/index.ts @@ -40,7 +40,7 @@ import { AddonModDataHelper, AddonModDatDisplayFieldsOptions } from '../../servi import { AddonModDataAutoSyncData, AddonModDataSyncResult } from '../../services/data-sync'; import { AddonModDataPrefetchHandler } from '../../services/handlers/prefetch-lazy'; import { AddonModDataComponentsCompileModule } from '../components-compile.module'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreTime } from '@singletons/time'; import { ADDON_MOD_DATA_AUTO_SYNCED, @@ -568,7 +568,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp this.analyticsLogEvent('mod_data_search_entries', { data: params, - url: CoreUrlUtils.addParamsToUrl(`/mod/data/view.php?d=${this.database.id}`, params), + url: CoreUrl.addParamsToUrl(`/mod/data/view.php?d=${this.database.id}`, params), }); } diff --git a/src/addons/mod/feedback/services/handlers/push-click.ts b/src/addons/mod/feedback/services/handlers/push-click.ts index 02ad4aedb..d6ae6a840 100644 --- a/src/addons/mod/feedback/services/handlers/push-click.ts +++ b/src/addons/mod/feedback/services/handlers/push-click.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonModFeedbackHelper } from '../feedback-helper'; @@ -48,7 +48,7 @@ export class AddonModFeedbackPushClickHandlerService implements CorePushNotifica * @inheritdoc */ handleClick(notification: AddonModFeedbackPushNotificationData): Promise { - const contextUrlParams = CoreUrlUtils.extractUrlParams(notification.contexturl!); + const contextUrlParams = CoreUrl.extractUrlParams(notification.contexturl!); const courseId = Number(notification.courseid); const moduleId = Number(contextUrlParams.id); diff --git a/src/addons/mod/forum/pages/search/search.ts b/src/addons/mod/forum/pages/search/search.ts index b60bd9ec6..89c701de2 100644 --- a/src/addons/mod/forum/pages/search/search.ts +++ b/src/addons/mod/forum/pages/search/search.ts @@ -27,7 +27,7 @@ import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; @@ -116,7 +116,7 @@ export class AddonModForumSearchPage implements OnInit { query, filters: JSON.stringify(this.resultsSource.getFilters()), }, - url: CoreUrlUtils.addParamsToUrl('/search/index.php', { + url: CoreUrl.addParamsToUrl('/search/index.php', { q: query, }), }); diff --git a/src/addons/mod/forum/services/forum.ts b/src/addons/mod/forum/services/forum.ts index 21ad741b5..3cec9605b 100644 --- a/src/addons/mod/forum/services/forum.ts +++ b/src/addons/mod/forum/services/forum.ts @@ -24,7 +24,7 @@ import { CoreNetwork } from '@services/network'; import { CoreFileEntry } from '@services/file-helper'; import { CoreGroups } from '@services/groups'; import { CoreSitesCommonWSOptions, CoreSites, CoreSitesReadingStrategy } from '@services/sites'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning, CoreWSStoredFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -1312,7 +1312,7 @@ export class AddonModForumProvider { protected translateWSPost(post: AddonModForumWSPost): AddonModForumPost { (post as unknown as AddonModForumPost).tags = (post.tags || []).map((tag) => { const viewUrl = (tag.urls && tag.urls.view) || ''; - const params = CoreUrlUtils.extractUrlParams(viewUrl); + const params = CoreUrl.extractUrlParams(viewUrl); return { id: tag.tagid, diff --git a/src/addons/mod/forum/services/handlers/push-click.ts b/src/addons/mod/forum/services/handlers/push-click.ts index 34f9a9662..1b17316fc 100644 --- a/src/addons/mod/forum/services/handlers/push-click.ts +++ b/src/addons/mod/forum/services/handlers/push-click.ts @@ -19,7 +19,7 @@ import { AddonModForum } from '@addons/mod/forum/services/forum'; import { CoreNavigator } from '@services/navigator'; import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; @@ -56,7 +56,7 @@ export class AddonModForumPushClickHandlerService implements CorePushNotificatio * @returns Promise resolved when done. */ async handleClick(notification: NotificationData): Promise { - const contextUrlParams = CoreUrlUtils.extractUrlParams(notification.contexturl); + const contextUrlParams = CoreUrl.extractUrlParams(notification.contexturl); const data = notification.customdata || {}; const courseId = Number(notification.courseid); const discussionId = Number(contextUrlParams.d || data.discussionid); diff --git a/src/addons/mod/lesson/pages/player/player.ts b/src/addons/mod/lesson/pages/player/player.ts index a17c77825..6c8228e11 100644 --- a/src/addons/mod/lesson/pages/player/player.ts +++ b/src/addons/mod/lesson/pages/player/player.ts @@ -23,7 +23,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSync } from '@services/sync'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile } from '@services/ws'; import { ModalController, Translate } from '@singletons'; @@ -439,7 +439,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave { // Format review lesson if present. if (this.eolData.reviewlesson) { - const params = CoreUrlUtils.extractUrlParams( this.eolData.reviewlesson.value); + const params = CoreUrl.extractUrlParams( this.eolData.reviewlesson.value); if (!params || !params.pageid) { // No pageid in the URL, the user cannot review (probably didn't answer any question). diff --git a/src/addons/mod/lesson/services/lesson-sync.ts b/src/addons/mod/lesson/services/lesson-sync.ts index 22da5ab5a..04db999a1 100644 --- a/src/addons/mod/lesson/services/lesson-sync.ts +++ b/src/addons/mod/lesson/services/lesson-sync.ts @@ -23,7 +23,7 @@ import { CoreNetwork } from '@services/network'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSync, CoreSyncResult } from '@services/sync'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; @@ -463,7 +463,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid // Mark the retake as finished in a sync if it can be reviewed. if (!ignoreBlock && response.data?.reviewlesson) { - const params = CoreUrlUtils.extractUrlParams( response.data.reviewlesson.value); + const params = CoreUrl.extractUrlParams( response.data.reviewlesson.value); if (params.pageid) { // The retake can be reviewed, mark it as finished. Don't block the user for this. this.setRetakeFinishedInSync(lessonId, retake.retake, Number(params.pageid), siteId); diff --git a/src/addons/mod/lti/services/lti.ts b/src/addons/mod/lti/services/lti.ts index ab3ca0547..f88d4c3b6 100644 --- a/src/addons/mod/lti/services/lti.ts +++ b/src/addons/mod/lti/services/lti.ts @@ -22,7 +22,7 @@ import { CoreFile } from '@services/file'; import { CorePlatform } from '@services/platform'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -244,7 +244,7 @@ export class AddonModLtiProvider { * @returns Promise resolved when the WS call is successful. */ async launch(url: string, params: AddonModLtiParam[]): Promise { - if (!CoreUrlUtils.isHttpURL(url)) { + if (!CoreUrl.isHttpURL(url)) { throw Translate.instant('addon.mod_lti.errorinvalidlaunchurl'); } diff --git a/src/addons/mod/quiz/services/handlers/push-click.ts b/src/addons/mod/quiz/services/handlers/push-click.ts index 3e196f289..22e7e10e4 100644 --- a/src/addons/mod/quiz/services/handlers/push-click.ts +++ b/src/addons/mod/quiz/services/handlers/push-click.ts @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonModQuiz } from '../quiz'; @@ -56,7 +56,7 @@ export class AddonModQuizPushClickHandlerService implements CorePushNotification * @returns Promise resolved when done. */ async handleClick(notification: AddonModQuizPushNotificationData): Promise { - const contextUrlParams = CoreUrlUtils.extractUrlParams(notification.contexturl || ''); + const contextUrlParams = CoreUrl.extractUrlParams(notification.contexturl || ''); const data = notification.customdata || {}; const courseId = Number(notification.courseid); diff --git a/src/addons/mod/scorm/services/scorm.ts b/src/addons/mod/scorm/services/scorm.ts index 927b59528..f0025d62e 100644 --- a/src/addons/mod/scorm/services/scorm.ts +++ b/src/addons/mod/scorm/services/scorm.ts @@ -23,7 +23,7 @@ import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@ import { CoreSync } from '@services/sync'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreWS, CoreWSExternalFile, CoreWSExternalWarning, CoreWSFile, CoreWSPreSets } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -1328,7 +1328,7 @@ export class AddonModScormProvider { protected isExternalLink(link: string): boolean { link = link.toLowerCase(); - if (link.match(/^https?:\/\//i) && !CoreUrlUtils.isLocalFileUrl(link)) { + if (link.match(/^https?:\/\//i) && !CoreUrl.isLocalFileUrl(link)) { return true; } else if (link.substring(0, 4) == 'www.') { return true; diff --git a/src/addons/mod/url/services/handlers/module.ts b/src/addons/mod/url/services/handlers/module.ts index 88c8bba52..cf4f0eae0 100644 --- a/src/addons/mod/url/services/handlers/module.ts +++ b/src/addons/mod/url/services/handlers/module.ts @@ -26,7 +26,7 @@ import { makeSingleton } from '@singletons'; import { AddonModUrl } from '../url'; import { AddonModUrlHelper } from '../url-helper'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { ADDON_MOD_URL_ADDON_NAME, ADDON_MOD_URL_MODNAME, ADDON_MOD_URL_PAGE_NAME } from '../../constants'; @@ -122,14 +122,14 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple return modIcon; } - const component = CoreUrlUtils.getThemeImageUrlParam(module.modicon, 'component'); + const component = CoreUrl.getThemeImageUrlParam(module.modicon, 'component'); if (component === this.modName) { return modIcon; } let icon: string | undefined; - let image = CoreUrlUtils.getThemeImageUrlParam(module.modicon, 'image'); + let image = CoreUrl.getThemeImageUrlParam(module.modicon, 'image'); if (image.startsWith('f/')) { // Remove prefix, and hyphen + numbered suffix. image = image.substring(2).replace(/-[0-9]+$/, ''); diff --git a/src/addons/notes/pages/list/list.ts b/src/addons/notes/pages/list/list.ts index 0976982c3..b31a6ac17 100644 --- a/src/addons/notes/pages/list/list.ts +++ b/src/addons/notes/pages/list/list.ts @@ -26,7 +26,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -313,7 +313,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy { ws: 'core_notes_view_notes', name: Translate.instant('addon.notes.notes'), data: { courseid: this.courseId, userid: this.userId || 0, category: 'notes' }, - url: CoreUrlUtils.addParamsToUrl('/notes/index.php', { + url: CoreUrl.addParamsToUrl('/notes/index.php', { user: this.userId, course: this.courseId !== CoreSites.getCurrentSiteHomeId() ? this.courseId : undefined, }), @@ -329,7 +329,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy { ws: 'core_notes_create_notes', name: Translate.instant('addon.notes.notes'), data: { courseid: this.courseId, userid: this.userId || 0, category: 'notes' }, - url: CoreUrlUtils.addParamsToUrl('/notes/edit.php', { + url: CoreUrl.addParamsToUrl('/notes/edit.php', { courseid: this.courseId, userid: this.userId, publishstate: this.type === 'personal' ? 'draft' : (this.type === 'course' ? 'public' : 'site'), diff --git a/src/core/classes/sites/authenticated-site.ts b/src/core/classes/sites/authenticated-site.ts index 25e98e451..20be3c9fe 100644 --- a/src/core/classes/sites/authenticated-site.ts +++ b/src/core/classes/sites/authenticated-site.ts @@ -39,7 +39,6 @@ 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'; import { CoreWait } from '@singletons/wait'; @@ -1268,11 +1267,33 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite { * * @param page Docs page to go to. * @returns Promise resolved with the Moodle docs URL. + * + * @deprecated since 4.5. Not needed anymore. */ - getDocsUrl(page?: string): Promise { + async getDocsUrl(page?: string): Promise { const release = this.infos?.release ? this.infos.release : undefined; + let docsUrl = 'https://docs.moodle.org/en/' + page; - return CoreUrlUtils.getDocsUrl(release, page); + if (release !== undefined) { + // Remove this part of the function if this file only uses CoreSites here. + const version = CoreSites.getMajorReleaseNumber(release).replace('.', ''); + + // Check is a valid number. + if (Number(version) >= 24) { + // Append release number. + docsUrl = docsUrl.replace('https://docs.moodle.org/', 'https://docs.moodle.org/' + version + '/'); + } + } + + try { + // Remove this part of the function if this file only uses CoreLang here. + let lang = CoreLang.getCurrentLanguageSync(CoreLangFormat.LMS); + lang = CoreLang.getParentLanguage() || lang; + + return docsUrl.replace('/en/', '/' + lang + '/'); + } catch { + return docsUrl; + } } /** diff --git a/src/core/classes/sites/site.ts b/src/core/classes/sites/site.ts index 80e6c07e3..f0bd5fe73 100644 --- a/src/core/classes/sites/site.ts +++ b/src/core/classes/sites/site.ts @@ -27,7 +27,7 @@ import { import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils, CoreUtilsOpenInBrowserOptions } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { SQLiteDB } from '@classes/sqlitedb'; @@ -380,7 +380,7 @@ export class CoreSite extends CoreAuthenticatedSite { const accessKey = this.tokenPluginFileWorks || this.tokenPluginFileWorks === undefined ? this.infos && this.infos.userprivateaccesskey : undefined; - return CoreUrlUtils.fixPluginfileURL(url, this.token || '', this.siteUrl, accessKey); + return CoreUrl.fixPluginfileURL(url, this.token || '', this.siteUrl, accessKey); } /** @@ -775,7 +775,7 @@ export class CoreSite extends CoreAuthenticatedSite { * @returns Promise resolved with boolean: whether it works or not. */ checkTokenPluginFile(url: string): Promise { - if (!CoreUrlUtils.canUseTokenPluginFile(url, this.siteUrl, this.infos && this.infos.userprivateaccesskey)) { + if (!CoreUrl.canUseTokenPluginFile(url, this.siteUrl, this.infos && this.infos.userprivateaccesskey)) { // Cannot use tokenpluginfile. return Promise.resolve(false); } else if (this.tokenPluginFileWorks !== undefined) { diff --git a/src/core/classes/sites/unauthenticated-site.ts b/src/core/classes/sites/unauthenticated-site.ts index 0f999406b..68ef3f975 100644 --- a/src/core/classes/sites/unauthenticated-site.ts +++ b/src/core/classes/sites/unauthenticated-site.ts @@ -17,7 +17,7 @@ import { CoreError } from '@classes/errors/error'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreSitesReadingStrategy } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreWS, CoreWSAjaxPreSets, CoreWSExternalWarning } from '@services/ws'; import { CorePath } from '@singletons/path'; @@ -37,7 +37,10 @@ export class CoreUnauthenticatedSite { * @param publicConfig Site public config. */ constructor(siteUrl: string, publicConfig?: CoreSitePublicConfigResponse) { - this.siteUrl = CoreUrlUtils.removeUrlParams(siteUrl); // Make sure the URL doesn't have params. + this.siteUrl = CoreUrl.removeUrlParts( + siteUrl, + [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment], + ); // Make sure the URL doesn't have params. if (publicConfig) { this.setPublicConfig(publicConfig); } @@ -143,7 +146,7 @@ export class CoreUnauthenticatedSite { * @returns URL with params. */ createSiteUrl(path: string, params?: Record, anchor?: string): string { - return CoreUrlUtils.addParamsToUrl(CorePath.concatenatePaths(this.siteUrl, path), params, anchor); + return CoreUrl.addParamsToUrl(CorePath.concatenatePaths(this.siteUrl, path), params, anchor); } /** @@ -157,8 +160,10 @@ export class CoreUnauthenticatedSite { return false; } - const siteUrl = CoreTextUtils.addEndingSlash(CoreUrlUtils.removeProtocolAndWWW(this.siteUrl)); - url = CoreTextUtils.addEndingSlash(CoreUrlUtils.removeProtocolAndWWW(url)); + const siteUrl = CoreTextUtils.addEndingSlash( + CoreUrl.removeUrlParts(this.siteUrl, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain]), + ); + url = CoreTextUtils.addEndingSlash(CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain])); return url.indexOf(siteUrl) == 0; } @@ -244,7 +249,10 @@ export class CoreUnauthenticatedSite { // Use the wwwroot returned by the server. if (config.httpswwwroot) { - this.siteUrl = CoreUrlUtils.removeUrlParams(config.httpswwwroot); // Make sure the URL doesn't have params. + this.siteUrl = CoreUrl.removeUrlParts( + config.httpswwwroot, + [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment], + ); // Make sure the URL doesn't have params. } return config; @@ -268,7 +276,7 @@ export class CoreUnauthenticatedSite { * @returns Whether it's a site file URL. */ isSitePluginFileUrl(url: string): boolean { - const isPluginFileUrl = CoreUrlUtils.isPluginFileUrl(url) || CoreUrlUtils.isTokenPluginFileUrl(url); + const isPluginFileUrl = CoreUrl.isPluginFileUrl(url) || CoreUrl.isTokenPluginFileUrl(url); if (!isPluginFileUrl) { return false; } @@ -283,7 +291,7 @@ export class CoreUnauthenticatedSite { * @returns Whether it's a site theme image URL. */ isSiteThemeImageUrl(url: string): boolean { - if (!CoreUrlUtils.isThemeImageUrl(url)) { + if (!CoreUrl.isThemeImageUrl(url)) { return false; } diff --git a/src/core/components/file/file.ts b/src/core/components/file/file.ts index 46dc1917d..a525a5ec2 100644 --- a/src/core/components/file/file.ts +++ b/src/core/components/file/file.ts @@ -20,7 +20,7 @@ import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; import { DownloadStatus } from '@/core/constants'; @@ -195,10 +195,10 @@ export class CoreFileComponent implements OnInit, OnDestroy { if (!this.canDownload || !this.state || this.state === DownloadStatus.NOT_DOWNLOADABLE) { // File cannot be downloaded, just open it. - if (CoreUrlUtils.isLocalFileUrl(this.fileUrl)) { + if (CoreUrl.isLocalFileUrl(this.fileUrl)) { CoreUtils.openFile(this.fileUrl); } else { - CoreUtils.openOnlineFile(CoreUrlUtils.unfixPluginfileURL(this.fileUrl)); + CoreUtils.openOnlineFile(CoreUrl.unfixPluginfileURL(this.fileUrl)); } return; diff --git a/src/core/components/iframe/iframe.ts b/src/core/components/iframe/iframe.ts index 32b7fc156..5012f9128 100644 --- a/src/core/components/iframe/iframe.ts +++ b/src/core/components/iframe/iframe.ts @@ -19,7 +19,7 @@ import { SafeResourceUrl } from '@angular/platform-browser'; import { CoreFile } from '@services/file'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreIframeUtils } from '@services/utils/iframe'; import { CoreUtils } from '@services/utils/utils'; import { DomSanitizer, Router, StatusBar } from '@singletons'; @@ -29,7 +29,6 @@ import { Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; import { NavigationStart } from '@angular/router'; import { CoreSites } from '@services/sites'; -import { CoreUrl } from '@singletons/url'; @Component({ selector: 'core-iframe', @@ -118,7 +117,7 @@ export class CoreIframeComponent implements OnChanges, OnDestroy { } // Show loading only with external URLs. - this.loading = !this.src || !CoreUrlUtils.isLocalFileUrl(this.src); + this.loading = !this.src || !CoreUrl.isLocalFileUrl(this.src); if (this.loading) { setTimeout(() => { @@ -197,8 +196,8 @@ export class CoreIframeComponent implements OnChanges, OnDestroy { this.launchExternalLabel = undefined; - if (url && !CoreUrlUtils.isLocalFileUrl(url)) { - url = CoreUrlUtils.getYoutubeEmbedUrl(url) || url; + if (url && !CoreUrl.isLocalFileUrl(url)) { + url = CoreUrl.getYoutubeEmbedUrl(url) || url; this.displayHelp = CoreIframeUtils.shouldDisplayHelpForUrl(url); const currentSite = CoreSites.getCurrentSite(); diff --git a/src/core/components/mod-icon/mod-icon.ts b/src/core/components/mod-icon/mod-icon.ts index 0f693cdc7..7e1a26cb8 100644 --- a/src/core/components/mod-icon/mod-icon.ts +++ b/src/core/components/mod-icon/mod-icon.ts @@ -28,7 +28,7 @@ import { CoreCourse } from '@features/course/services/course'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreSites } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; const assetsPath = 'assets/img/'; const fallbackModName = 'external-tool'; @@ -155,8 +155,8 @@ export class CoreModIconComponent implements OnInit, OnChanges { } // If it's an Moodle Theme icon, check if filtericon is set and use it. - if (CoreUrlUtils.isThemeImageUrl(this.iconUrl())) { - const filter = CoreUrlUtils.getThemeImageUrlParam(this.iconUrl(), 'filtericon'); + if (CoreUrl.isThemeImageUrl(this.iconUrl())) { + const filter = CoreUrl.getThemeImageUrlParam(this.iconUrl(), 'filtericon'); if (filter === '1') { this.brandedClass = false; @@ -233,7 +233,7 @@ export class CoreModIconComponent implements OnInit, OnChanges { * @returns Guessed modname. */ protected getComponentNameFromIconUrl(iconUrl: string): string { - const component = CoreUrlUtils.getThemeImageUrlParam(iconUrl, 'component'); + const component = CoreUrl.getThemeImageUrlParam(iconUrl, 'component'); // Some invalid components (others may be added later on). if (component === 'core' || component === 'theme') { diff --git a/src/core/components/user-avatar/user-avatar.ts b/src/core/components/user-avatar/user-avatar.ts index d54342cf8..cc85a0f4c 100644 --- a/src/core/components/user-avatar/user-avatar.ts +++ b/src/core/components/user-avatar/user-avatar.ts @@ -21,7 +21,7 @@ import { USER_PROFILE_PICTURE_UPDATED, CoreUserBasicData } from '@features/user/ import { CoreNavigator } from '@services/navigator'; import { CoreNetwork } from '@services/network'; import { CoreUserHelper } from '@features/user/services/user-helper'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreSiteInfo } from '@classes/sites/unauthenticated-site'; /** @@ -124,7 +124,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { this.fullname = this.fullname || (this.user && (this.user.fullname || this.user.userfullname)); - if (this.avatarUrl && CoreUrlUtils.isThemeImageUrl(this.avatarUrl)) { + if (this.avatarUrl && CoreUrl.isThemeImageUrl(this.avatarUrl)) { this.avatarUrl = undefined; } diff --git a/src/core/directives/external-content.ts b/src/core/directives/external-content.ts index dbd917e92..94c02458b 100644 --- a/src/core/directives/external-content.ts +++ b/src/core/directives/external-content.ts @@ -26,7 +26,7 @@ import { import { CoreFile, CoreFileProvider } from '@services/file'; import { CoreFilepool, CoreFilepoolFileActions, CoreFilepoolFileEventData } from '@services/filepool'; import { CoreSites } from '@services/sites'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreLogger } from '@singletons/logger'; import { CoreError } from '@classes/errors/error'; @@ -206,8 +206,8 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O const site = await CoreUtils.ignoreErrors(CoreSites.getSite(this.siteId)); const isSiteFile = site?.isSitePluginFileUrl(url); - if (!url || !url.match(/^https?:\/\//i) || CoreUrlUtils.isLocalFileUrl(url) || - (tagName === 'A' && !(isSiteFile || site?.isSiteThemeImageUrl(url) || CoreUrlUtils.isGravatarUrl(url)))) { + if (!url || !url.match(/^https?:\/\//i) || CoreUrl.isLocalFileUrl(url) || + (tagName === 'A' && !(isSiteFile || site?.isSiteThemeImageUrl(url) || CoreUrl.isGravatarUrl(url)))) { this.logger.debug('Ignoring non-downloadable URL: ' + url); @@ -393,7 +393,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O finalUrl = CoreFile.convertFileSrc(finalUrl); } - if (!CoreUrlUtils.isLocalFileUrl(finalUrl) && !finalUrl.includes('#') && tagName !== 'A') { + if (!CoreUrl.isLocalFileUrl(finalUrl) && !finalUrl.includes('#') && tagName !== 'A') { /* In iOS, if we use the same URL in embedded file and background download then the download only downloads a few bytes (cached ones). Add an anchor to the URL so both URLs are different. Don't add this anchor if the URL already has an anchor, otherwise other anchors might not work. diff --git a/src/core/directives/link.ts b/src/core/directives/link.ts index ca6eeca1d..0c7e72553 100644 --- a/src/core/directives/link.ts +++ b/src/core/directives/link.ts @@ -19,7 +19,7 @@ import { IonContent } from '@ionic/angular'; import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; import { CoreConstants } from '@/core/constants'; @@ -27,7 +27,6 @@ import { CoreContentLinksHelper } from '@features/contentlinks/services/contentl import { CoreCustomURLSchemes } from '@services/urlschemes'; import { DomSanitizer } from '@singletons'; import { CoreFilepool } from '@services/filepool'; -import { CoreUrl } from '@singletons/url'; import { CoreDom } from '@singletons/dom'; /** @@ -87,7 +86,7 @@ export class CoreLinkDirective implements OnInit { href = href || this.element.getAttribute('href') || this.element.getAttribute('xlink:href'); - if (!href || CoreUrlUtils.getUrlScheme(href) === 'javascript') { + if (!href || CoreUrl.getUrlProtocol(href) === 'javascript') { return; } @@ -116,7 +115,7 @@ export class CoreLinkDirective implements OnInit { */ protected async navigate(href: string, openIn?: string | null): Promise { - if (CoreUrlUtils.isLocalFileUrl(href)) { + if (CoreUrl.isLocalFileUrl(href)) { return this.openLocalFile(href); } diff --git a/src/core/features/contentlinks/services/contentlinks-delegate.ts b/src/core/features/contentlinks/services/contentlinks-delegate.ts index 0c2fa6e22..c76527243 100644 --- a/src/core/features/contentlinks/services/contentlinks-delegate.ts +++ b/src/core/features/contentlinks/services/contentlinks-delegate.ts @@ -15,11 +15,10 @@ import { Injectable } from '@angular/core'; import { CoreLogger } from '@singletons/logger'; import { CoreSites } from '@services/sites'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { CoreText } from '@singletons/text'; -import { CoreUrl } from '@singletons/url'; /** * Interface that all handlers must implement. @@ -174,7 +173,7 @@ export class CoreContentLinksDelegateService { const linkActions: CoreContentLinksHandlerActions[] = []; const promises: Promise[] = []; - const params = CoreUrlUtils.extractUrlParams(url); + const params = CoreUrl.extractUrlParams(url); const relativeUrl = CoreText.addStartingSlash(CoreUrl.toRelativeURL(site.getURL(), url)); for (const name in this.handlers) { diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts index 27fd778b0..24de44d23 100644 --- a/src/core/features/course/classes/main-resource-component.ts +++ b/src/core/features/course/classes/main-resource-component.ts @@ -31,7 +31,7 @@ import { CoreCourseHelper, CoreCourseModuleData } from '../services/course-helpe import { CoreCourseModuleDelegate, CoreCourseModuleMainComponent } from '../services/module-delegate'; import { CoreCourseModulePrefetchDelegate } from '../services/module-prefetch-delegate'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreTime } from '@singletons/time'; /** @@ -506,7 +506,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, url = options.url; } else if (this.pluginName) { // Use default value. - url = CoreUrlUtils.addParamsToUrl(`/mod/${this.pluginName}/view.php?id=${this.module.id}`, options.data); + url = CoreUrl.addParamsToUrl(`/mod/${this.pluginName}/view.php?id=${this.module.id}`, options.data); } } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 0636e5048..b3e31546f 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -59,7 +59,7 @@ import { CoreFileHelper } from '@services/file-helper'; import { CoreNetwork } from '@services/network'; import { CoreSite } from '@classes/sites/site'; import { CoreFile } from '@services/file'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreFilterHelper } from '@features/filter/services/filter-helper'; @@ -710,7 +710,7 @@ export class CoreCourseHelperProvider { options, ); - if (CoreUrlUtils.isLocalFileUrl(result.path)) { + if (CoreUrl.isLocalFileUrl(result.path)) { return CoreUtils.openFile(result.path, options); } diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts index ced2b6dcf..c611b2e4d 100644 --- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts +++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts @@ -31,7 +31,7 @@ import { Subscription } from 'rxjs'; import { CoreSites } from '@services/sites'; import { CoreFilepool } from '@services/filepool'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreEventFormActionData, CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEditorOffline } from '../../services/editor-offline'; @@ -514,7 +514,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, const url = el.src; - if (!url || !CoreUrlUtils.isDownloadableUrl(url) || (!canDownloadFiles && site?.isSitePluginFileUrl(url))) { + if (!url || !CoreUrl.isDownloadableUrl(url) || (!canDownloadFiles && site?.isSitePluginFileUrl(url))) { // Nothing to treat. return; } diff --git a/src/core/features/grades/services/grades-helper.ts b/src/core/features/grades/services/grades-helper.ts index 4a836cca5..ab09a7bb1 100644 --- a/src/core/features/grades/services/grades-helper.ts +++ b/src/core/features/grades/services/grades-helper.ts @@ -34,7 +34,7 @@ import { CoreGradesTableRow, } from '@features/grades/services/grades'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreMenuItem, CoreUtils } from '@services/utils/utils'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreNavigator } from '@services/navigator'; @@ -416,7 +416,7 @@ export class CoreGradesHelperProvider { const matches = row.itemname.content.match(regex); if (matches && matches.length) { - const hrefParams = CoreUrlUtils.extractUrlParams(matches[1]); + const hrefParams = CoreUrl.extractUrlParams(matches[1]); return hrefParams && parseInt(hrefParams.id) === moduleId; } diff --git a/src/core/features/h5p/classes/player.ts b/src/core/features/h5p/classes/player.ts index 917c0fd3b..3dcdadb60 100644 --- a/src/core/features/h5p/classes/player.ts +++ b/src/core/features/h5p/classes/player.ts @@ -14,7 +14,7 @@ import { CoreFile } from '@services/file'; import { CoreSites } from '@services/sites'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreH5P } from '../services/h5p'; import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core'; @@ -51,7 +51,7 @@ export class CoreH5PPlayer { params.component = component; } - return CoreUrlUtils.addParamsToUrl(CorePath.concatenatePaths(siteUrl, '/h5p/embed.php'), params); + return CoreUrl.addParamsToUrl(CorePath.concatenatePaths(siteUrl, '/h5p/embed.php'), params); } /** @@ -303,7 +303,7 @@ export class CoreH5PPlayer { params.customCssUrl = customCssUrl; } - return CoreUrlUtils.addParamsToUrl(path, params); + return CoreUrl.addParamsToUrl(path, params); } /** diff --git a/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts b/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts index 1aef7bcd9..0fd4d67b3 100644 --- a/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts +++ b/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts @@ -22,7 +22,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreH5P } from '@features/h5p/services/h5p'; import { DownloadStatus } from '@/core/constants'; import { CoreSite } from '@classes/sites/site'; @@ -140,7 +140,7 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy { ); // Add the preventredirect param so the user can authenticate. - this.iframeSrc = CoreUrlUtils.addParamsToUrl(src, { preventredirect: false }); + this.iframeSrc = CoreUrl.addParamsToUrl(src, { preventredirect: false }); } } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading H5P package.', true); diff --git a/src/core/features/h5p/components/h5p-player/h5p-player.ts b/src/core/features/h5p/components/h5p-player/h5p-player.ts index cf6770813..49227a1f9 100644 --- a/src/core/features/h5p/components/h5p-player/h5p-player.ts +++ b/src/core/features/h5p/components/h5p-player/h5p-player.ts @@ -18,7 +18,7 @@ import { CoreNetwork } from '@services/network'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { DownloadStatus } from '@/core/constants'; import { CoreSite } from '@classes/sites/site'; @@ -168,7 +168,7 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy { */ protected async checkCanDownload(): Promise { this.observer && this.observer.off(); - this.urlParams = CoreUrlUtils.extractUrlParams(this.src || ''); + this.urlParams = CoreUrl.extractUrlParams(this.src || ''); if (this.src && this.siteCanDownload && CoreH5P.canGetTrustedH5PFileInSite() && this.site.containsUrl(this.src)) { this.calculateState(); diff --git a/src/core/features/h5p/services/h5p.ts b/src/core/features/h5p/services/h5p.ts index de53844e0..5681020ad 100644 --- a/src/core/features/h5p/services/h5p.ts +++ b/src/core/features/h5p/services/h5p.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; import { CoreWSExternalWarning, CoreWSExternalFile, CoreWSFile } from '@services/ws'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreQueueRunner } from '@classes/queue-runner'; import { CoreSite } from '@classes/sites/site'; @@ -246,7 +246,7 @@ export class CoreH5PProvider { url = url.replace('/webservice/pluginfile', '/pluginfile'); } - return CoreUrlUtils.removeUrlParams(url); + return CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment]); } } diff --git a/src/core/features/h5p/services/handlers/pluginfile.ts b/src/core/features/h5p/services/handlers/pluginfile.ts index a370b7cbb..3c3f6a513 100644 --- a/src/core/features/h5p/services/handlers/pluginfile.ts +++ b/src/core/features/h5p/services/handlers/pluginfile.ts @@ -19,7 +19,7 @@ import { CoreFilepoolOnProgressCallback } from '@services/filepool'; import { CorePluginFileDownloadableResult, CorePluginFileHandler } from '@services/plugin-file-delegate'; import { CoreSites } from '@services/sites'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSFile } from '@services/ws'; import { CoreH5P } from '../h5p'; @@ -80,7 +80,7 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler { const urls: string[] = []; for (let i = 0; i < iframes.length; i++) { - const params = CoreUrlUtils.extractUrlParams(iframes[i].src); + const params = CoreUrl.extractUrlParams(iframes[i].src); if (params.url) { urls.push(params.url); diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index 9702f05c4..b127025c0 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -28,8 +28,7 @@ import { import { CoreError } from '@classes/errors/error'; import { CoreConstants } from '@/core/constants'; import { Translate } from '@singletons'; -import { CoreUrl } from '@singletons/url'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreNavigator } from '@services/navigator'; import { CoreCustomURLSchemes, CoreCustomURLSchemesHandleError } from '@services/urlschemes'; import { CoreTextUtils } from '@services/utils/text'; @@ -206,7 +205,9 @@ export class CoreLoginSitePage implements OnInit { */ protected extendCoreLoginSiteInfo(sites: CoreLoginSiteInfoExtended[]): CoreLoginSiteInfoExtended[] { return sites.map((site) => { - site.noProtocolUrl = this.siteFinderSettings.displayurl && site.url ? CoreUrl.removeProtocol(site.url) : ''; + site.noProtocolUrl = this.siteFinderSettings.displayurl && site.url + ? CoreUrl.removeUrlParts(site.url, CoreUrlPartNames.Protocol) + : ''; const name = this.siteFinderSettings.displaysitename ? site.name : ''; const alias = this.siteFinderSettings.displayalias && site.alias ? site.alias : ''; @@ -510,7 +511,7 @@ export class CoreLoginSitePage implements OnInit { name: 'connect', title: '', location: '', - noProtocolUrl: CoreUrl.removeProtocol(search), + noProtocolUrl: CoreUrl.removeUrlParts(search, CoreUrlPartNames.Protocol), }; } else { this.enteredSiteUrl = undefined; @@ -563,7 +564,7 @@ export class CoreLoginSitePage implements OnInit { } // Not a custom URL scheme, check if it's a URL scheme to another app. - const scheme = CoreUrlUtils.getUrlProtocol(text); + const scheme = CoreUrl.getUrlProtocol(text); if (scheme && scheme != 'http' && scheme != 'https') { CoreDomUtils.showErrorModal(Translate.instant('core.errorurlschemeinvalidscheme', { $a: text })); diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index 5a9917de1..856d729ca 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -23,7 +23,6 @@ import { CoreSites, CoreLoginSiteInfo, CoreSiteBasicInfo } from '@services/sites import { CoreWS, CoreWSExternalWarning } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlParams, CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { CoreSite } from '@classes/sites/site'; @@ -31,7 +30,7 @@ import { CoreError } from '@classes/errors/error'; import { CoreWSError } from '@classes/errors/wserror'; import { DomSanitizer, makeSingleton, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; -import { CoreUrl } from '@singletons/url'; +import { CoreUrl, CoreUrlParams } from '@singletons/url'; import { CoreNavigator, CoreRedirectPayload } from '@services/navigator'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreCustomURLSchemes } from '@services/urlschemes'; @@ -356,7 +355,7 @@ export class CoreLoginHelperProvider { if (siteConfig.identityproviders && siteConfig.identityproviders.length) { siteConfig.identityproviders.forEach((provider) => { - const urlParams = CoreUrlUtils.extractUrlParams(provider.url); + const urlParams = CoreUrl.extractUrlParams(provider.url); if ( provider.url && @@ -397,7 +396,7 @@ export class CoreLoginHelperProvider { if (siteConfig.identityproviders && siteConfig.identityproviders.length) { siteConfig.identityproviders.forEach((provider) => { - const urlParams = CoreUrlUtils.extractUrlParams(provider.url); + const urlParams = CoreUrl.extractUrlParams(provider.url); if (provider.url && (provider.url.indexOf(httpsUrl) != -1 || provider.url.indexOf(httpUrl) != -1) && !site.isFeatureDisabled(IDENTITY_PROVIDER_FEATURE_NAME_PREFIX + urlParams.id)) { @@ -642,7 +641,7 @@ export class CoreLoginHelperProvider { return false; } - const params = CoreUrlUtils.extractUrlParams(provider.url); + const params = CoreUrl.extractUrlParams(provider.url); if (!params.id) { return false; @@ -830,7 +829,7 @@ export class CoreLoginHelperProvider { loginUrl += '&urlscheme=' + CoreConstants.CONFIG.customurlscheme; if (urlParams) { - loginUrl = CoreUrlUtils.addParamsToUrl(loginUrl, urlParams); + loginUrl = CoreUrl.addParamsToUrl(loginUrl, urlParams); } // Store the siteurl and passport in CoreConfigProvider for persistence. @@ -1334,7 +1333,7 @@ export class CoreLoginHelperProvider { } } else if (text) { // Not a custom URL scheme, check if it's a URL scheme to another app. - const scheme = CoreUrlUtils.getUrlProtocol(text); + const scheme = CoreUrl.getUrlProtocol(text); if (scheme && scheme != 'http' && scheme != 'https') { CoreDomUtils.showErrorModal(Translate.instant('core.errorurlschemeinvalidscheme', { $a: text })); diff --git a/src/core/features/policy/pages/site-policy/site-policy.ts b/src/core/features/policy/pages/site-policy/site-policy.ts index bcb986566..46e65efb1 100644 --- a/src/core/features/policy/pages/site-policy/site-policy.ts +++ b/src/core/features/policy/pages/site-policy/site-policy.ts @@ -25,7 +25,7 @@ import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { Translate } from '@singletons'; import { CorePolicy, CorePolicyAgreementStyle, CorePolicySitePolicy } from '@features/policy/services/policy'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { IonContent } from '@ionic/angular'; import { CoreScreen } from '@services/screen'; import { Subscription } from 'rxjs'; @@ -223,7 +223,7 @@ export class CorePolicySitePolicyPage implements OnInit, OnDestroy { ws: 'tool_policy_get_user_acceptances', name: this.currentPolicy.name, data: analyticsParams, - url: CoreUrlUtils.addParamsToUrl('/admin/tool/policy/view.php', analyticsParams), + url: CoreUrl.addParamsToUrl('/admin/tool/policy/view.php', analyticsParams), }); } @@ -236,7 +236,7 @@ export class CorePolicySitePolicyPage implements OnInit, OnDestroy { ws: 'tool_policy_get_user_acceptances', name: Translate.instant('core.policy.consentpagetitle'), data: {}, - url: CoreUrlUtils.addParamsToUrl('/admin/tool/policy/index.php'), + url: CoreUrl.addParamsToUrl('/admin/tool/policy/index.php'), }); } diff --git a/src/core/features/question/classes/base-question-component.ts b/src/core/features/question/classes/base-question-component.ts index 2bcfcc5ba..09642ba35 100644 --- a/src/core/features/question/classes/base-question-component.ts +++ b/src/core/features/question/classes/base-question-component.ts @@ -18,7 +18,7 @@ import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreWSFile } from '@services/ws'; import { CoreIonicColorNames } from '@singletons/colors'; import { CoreLogger } from '@singletons/logger'; @@ -367,7 +367,7 @@ export class CoreQuestionBaseComponent -1) { + if (CoreUrl.isThemeImageUrl(fileUrl) && fileUrl.indexOf('flagged') > -1) { // Ignore flag images. return; } diff --git a/src/core/features/search/pages/global-search/global-search.ts b/src/core/features/search/pages/global-search/global-search.ts index bd5b28e7e..c67d2abfc 100644 --- a/src/core/features/search/pages/global-search/global-search.ts +++ b/src/core/features/search/pages/global-search/global-search.ts @@ -19,7 +19,7 @@ import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { Translate } from '@singletons'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreEvents, CoreEventObserver } from '@singletons/events'; import { CoreSearchGlobalSearchResult, @@ -115,7 +115,7 @@ export class CoreSearchGlobalSearchPage implements OnInit, OnDestroy, AfterViewI query, filters: JSON.stringify(this.resultsSource.getFilters()), }, - url: CoreUrlUtils.addParamsToUrl('/search/index.php', { + url: CoreUrl.addParamsToUrl('/search/index.php', { q: query, }), }); diff --git a/src/core/features/siteplugins/services/siteplugins-init.ts b/src/core/features/siteplugins/services/siteplugins-init.ts index fc4854919..e85dd2902 100644 --- a/src/core/features/siteplugins/services/siteplugins-init.ts +++ b/src/core/features/siteplugins/services/siteplugins-init.ts @@ -84,7 +84,7 @@ import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/class import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; import { CoreObject } from '@singletons/object'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CorePath } from '@singletons/path'; import { CoreEnrolAction, CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate'; import { CoreSitePluginsEnrolHandler } from '../classes/handlers/enrol-handler'; @@ -170,7 +170,7 @@ export class CoreSitePluginsInitService { // Make sure it's an absolute URL. Do not use toAbsoluteURL because it can change the behaviour and break plugin styles. let url = handlerSchema.styles?.url; - if (url && !CoreUrlUtils.isAbsoluteURL(url)) { + if (url && !CoreUrl.isAbsoluteURL(url)) { url = CorePath.concatenatePaths(site.getURL(), url); } diff --git a/src/core/features/tag/pages/index/index.ts b/src/core/features/tag/pages/index/index.ts index fba315863..bc944e34a 100644 --- a/src/core/features/tag/pages/index/index.ts +++ b/src/core/features/tag/pages/index/index.ts @@ -21,7 +21,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreTime } from '@singletons/time'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { Translate } from '@singletons'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; /** * Page that displays the tag index. @@ -62,7 +62,7 @@ export class CoreTagIndexPage implements OnInit { ws: 'core_tag_get_tagindex_per_area', name: this.tagName || Translate.instant('core.tag.tag'), data: { id: this.tagId || undefined, ...params, category: 'tag' }, - url: CoreUrlUtils.addParamsToUrl('/tag/index.php', params), + url: CoreUrl.addParamsToUrl('/tag/index.php', params), }); }); } diff --git a/src/core/features/user/pages/about/about.ts b/src/core/features/user/pages/about/about.ts index 4315f0843..e5d3c71ef 100644 --- a/src/core/features/user/pages/about/about.ts +++ b/src/core/features/user/pages/about/about.ts @@ -32,7 +32,7 @@ import { CoreSite } from '@classes/sites/site'; import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { Translate } from '@singletons'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; /** * Page that displays info about a user. @@ -247,7 +247,7 @@ export class CoreUserAboutPage implements OnInit, OnDestroy { return 'undefined'; } - if (CoreUrlUtils.isThemeImageUrl(avatarUrl, this.site?.siteUrl)) { + if (CoreUrl.isThemeImageUrl(avatarUrl, this.site?.siteUrl)) { return 'default'; } diff --git a/src/core/features/user/services/user.ts b/src/core/features/user/services/user.ts index 9ac7bcd63..e837294d5 100644 --- a/src/core/features/user/services/user.ts +++ b/src/core/features/user/services/user.ts @@ -26,7 +26,7 @@ import { CoreEvents, CoreEventSiteData, CoreEventUserDeletedData, CoreEventUserS import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; import { CoreError } from '@classes/errors/error'; import { USERS_TABLE_NAME, CoreUserDBRecord } from './database/user'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; import { CoreConstants } from '@/core/constants'; @@ -668,7 +668,7 @@ export class CoreUserProvider { } // Do not prefetch when initials are set and image is default. - if (imageUrl && CoreUrlUtils.isThemeImageUrl(imageUrl)) { + if (imageUrl && CoreUrl.isThemeImageUrl(imageUrl)) { return; } diff --git a/src/core/initializers/prepare-inapp-browser.ts b/src/core/initializers/prepare-inapp-browser.ts index 18be057f6..e3c393eb7 100644 --- a/src/core/initializers/prepare-inapp-browser.ts +++ b/src/core/initializers/prepare-inapp-browser.ts @@ -20,7 +20,7 @@ import { CorePlatform } from '@services/platform'; import { CoreSites } from '@services/sites'; import { CoreCustomURLSchemes } from '@services/urlschemes'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; @@ -34,9 +34,9 @@ export default function(): void { // Check URLs loaded in any InAppBrowser. CoreEvents.on(CoreEvents.IAB_LOAD_START, async (event) => { // URLs with a custom scheme can be prefixed with "http://" or "https://", we need to remove this. - const protocol = CoreUrlUtils.getUrlProtocol(event.url); + const protocol = CoreUrl.getUrlProtocol(event.url); const url = event.url.replace(/^https?:\/\//, ''); - const urlScheme = CoreUrlUtils.getUrlProtocol(url); + const urlScheme = CoreUrl.getUrlProtocol(url); const isExternalApp = urlScheme && urlScheme !== 'file' && urlScheme !== 'cdvfile'; if (CoreCustomURLSchemes.isCustomURL(url)) { diff --git a/src/core/services/analytics.ts b/src/core/services/analytics.ts index 0fb057e96..91b6520e5 100644 --- a/src/core/services/analytics.ts +++ b/src/core/services/analytics.ts @@ -20,8 +20,8 @@ import { CoreEvents } from '@singletons/events'; import { CoreSites } from './sites'; import { CoreConfig, CoreConfigProvider } from './config'; import { CoreConstants } from '../constants'; -import { CoreUrlUtils } from './utils/url'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreUrl } from '@singletons/url'; /** * Helper service to support analytics. @@ -107,7 +107,7 @@ export class CoreAnalyticsService extends CoreDelegate { } if ('url' in treatedEvent && treatedEvent.url) { - if (!CoreUrlUtils.isAbsoluteURL(treatedEvent.url)) { + if (!CoreUrl.isAbsoluteURL(treatedEvent.url)) { treatedEvent.url = site.createSiteUrl(treatedEvent.url); } else if (!site.containsUrl(treatedEvent.url)) { // URL belongs to a different site, ignore the event. diff --git a/src/core/services/file-helper.ts b/src/core/services/file-helper.ts index d756ef177..91163f2ad 100644 --- a/src/core/services/file-helper.ts +++ b/src/core/services/file-helper.ts @@ -21,7 +21,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; import { CoreWS, CoreWSFile } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils'; import { CoreConstants, DownloadStatus } from '@/core/constants'; import { CoreError } from '@classes/errors/error'; @@ -93,7 +93,7 @@ export class CoreFileHelperProvider { return; } - if (!CoreUrlUtils.isLocalFileUrl(url)) { + if (!CoreUrl.isLocalFileUrl(url)) { /* In iOS, if we use the same URL in embedded browser and background download then the download only downloads a few bytes (cached ones). Add a hash to the URL so both URLs are different. */ url = url + '#moodlemobile-embedded'; diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 9455dfbfb..7e0b3f450 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -26,7 +26,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreUtils, CoreUtilsOpenFileOptions } from '@services/utils/utils'; import { CoreError } from '@classes/errors/error'; import { DownloadStatus } from '@/core/constants'; @@ -51,7 +51,6 @@ import { QUEUE_TABLE_PRIMARY_KEYS, } from '@services/database/filepool'; import { CoreFileHelper } from './file-helper'; -import { CoreUrl } from '@singletons/url'; import { CoreDatabaseTable } from '@classes/database/database-table'; import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; import { lazyMap, LazyMap } from '../utils/lazy-map'; @@ -782,7 +781,7 @@ export class CoreFilepoolProvider { CoreAnalytics.logEvent({ type: CoreAnalyticsEventType.DOWNLOAD_FILE, - fileUrl: CoreUrlUtils.unfixPluginfileURL(fileUrl, site.getURL()), + fileUrl: CoreUrl.unfixPluginfileURL(fileUrl, site.getURL()), }); // Add the anchor again to the local URL. @@ -1126,14 +1125,14 @@ export class CoreFilepoolProvider { const element = elements[i]; const url = 'href' in element ? element.href : element.src; - if (url && CoreUrlUtils.isDownloadableUrl(url) && urls.indexOf(url) == -1) { + if (url && CoreUrl.isDownloadableUrl(url) && urls.indexOf(url) == -1) { urls.push(url); } // Treat video poster. if (element.tagName == 'VIDEO' && element.getAttribute('poster')) { const poster = element.getAttribute('poster'); - if (poster && CoreUrlUtils.isDownloadableUrl(poster) && urls.indexOf(poster) == -1) { + if (poster && CoreUrl.isDownloadableUrl(poster) && urls.indexOf(poster) == -1) { urls.push(poster); } } @@ -1363,7 +1362,7 @@ export class CoreFilepoolProvider { } // Remove the anchor. - url = CoreUrl.removeUrlAnchor(url); + url = CoreUrl.removeUrlParts(url, CoreUrlPartNames.Fragment); // Try to guess the filename the target file should have. // We want to keep the original file name so people can easily identify the files after the download. @@ -1535,7 +1534,7 @@ export class CoreFilepoolProvider { return DownloadStatus.NOT_DOWNLOADABLE; } - fileUrl = CoreUrl.removeUrlAnchor(CoreFileHelper.getFileUrl(file)); + fileUrl = CoreUrl.removeUrlParts(CoreFileHelper.getFileUrl(file), CoreUrlPartNames.Fragment); timemodified = file.timemodified ?? timemodified; revision = revision ?? this.getRevisionFromUrl(fileUrl); const fileId = this.getFileIdByUrl(fileUrl); @@ -1914,7 +1913,7 @@ export class CoreFilepoolProvider { * @returns The args found, undefined if not a pluginfile. */ protected getPluginFileArgs(url: string): string[] | undefined { - if (!CoreUrlUtils.isPluginFileUrl(url)) { + if (!CoreUrl.isPluginFileUrl(url)) { // Not pluginfile, return. return; } @@ -2160,27 +2159,27 @@ export class CoreFilepoolProvider { if (fileUrl.indexOf('/webservice/pluginfile') !== -1) { // It's a pluginfile URL. Search for the 'file' param to extract the name. - const params = CoreUrlUtils.extractUrlParams(fileUrl); + const params = CoreUrl.extractUrlParams(fileUrl); if (params.file) { filename = params.file.substring(params.file.lastIndexOf('/') + 1); } else { // 'file' param not found. Extract what's after the last '/' without params. - filename = CoreUrlUtils.getLastFileWithoutParams(fileUrl); + filename = CoreUrl.getLastFileWithoutParams(fileUrl); } - } else if (CoreUrlUtils.isGravatarUrl(fileUrl)) { + } else if (CoreUrl.isGravatarUrl(fileUrl)) { // Extract gravatar ID. - filename = 'gravatar_' + CoreUrlUtils.getLastFileWithoutParams(fileUrl); - } else if (CoreUrlUtils.isThemeImageUrl(fileUrl)) { + filename = 'gravatar_' + CoreUrl.getLastFileWithoutParams(fileUrl); + } else if (CoreUrl.isThemeImageUrl(fileUrl)) { // Extract user ID. const matches = fileUrl.match(/\/core\/([^/]*)\//); if (matches && matches[1]) { filename = matches[1]; } // Attach a constant and the image type. - filename = 'default_' + filename + '_' + CoreUrlUtils.getLastFileWithoutParams(fileUrl); + filename = 'default_' + filename + '_' + CoreUrl.getLastFileWithoutParams(fileUrl); } else { // Another URL. Just get what's after the last /. - filename = CoreUrlUtils.getLastFileWithoutParams(fileUrl); + filename = CoreUrl.getLastFileWithoutParams(fileUrl); } // If there are hashes in the URL, extract them. @@ -3005,7 +3004,7 @@ export class CoreFilepoolProvider { try { let fileUrl = absoluteUrl; - if (!CoreUrlUtils.isLocalFileUrl(absoluteUrl)) { + if (!CoreUrl.isLocalFileUrl(absoluteUrl)) { // Not a local file, download it. fileUrl = await this.downloadUrl( siteId, diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts index 82c5a906d..5bb2c0be2 100644 --- a/src/core/services/navigator.ts +++ b/src/core/services/navigator.ts @@ -23,7 +23,7 @@ import { CoreMainMenu } from '@features/mainmenu/services/mainmenu'; import { CoreObject } from '@singletons/object'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreTextUtils } from '@services/utils/text'; import { makeSingleton, NavController, Router } from '@singletons'; import { CoreScreen } from './screen'; @@ -262,7 +262,7 @@ export class CoreNavigatorService { * @returns Current path. */ getCurrentPath(): string { - return CoreUrlUtils.removeUrlParams(Router.url); + return CoreUrl.removeUrlParts(Router.url, [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment]); } /** diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index e2d80c06f..8433311e9 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -21,7 +21,7 @@ import { CoreEvents } from '@singletons/events'; import { CoreWS } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { @@ -286,9 +286,9 @@ export class CoreSitesProvider { */ async checkSite(siteUrl: string, protocol: string = 'https://'): Promise { // The formatURL function adds the protocol if is missing. - siteUrl = CoreUrlUtils.formatURL(siteUrl); + siteUrl = CoreUrl.formatURL(siteUrl); - if (!CoreUrlUtils.isHttpURL(siteUrl)) { + if (!CoreUrl.isHttpURL(siteUrl)) { throw new CoreError(Translate.instant('core.login.invalidsite')); } @@ -350,7 +350,7 @@ export class CoreSitesProvider { } // Try to add or remove 'www'. - temporarySite.setURL(CoreUrlUtils.addOrRemoveWWW(temporarySite.getURL())); + temporarySite.setURL(CoreUrl.addOrRemoveWWW(temporarySite.getURL())); try { config = await temporarySite.getPublicConfig(); @@ -546,7 +546,7 @@ export class CoreSitesProvider { // We only allow one retry (to avoid loops). if (!retry && data.errorcode == 'requirecorrectaccess') { - siteUrl = CoreUrlUtils.addOrRemoveWWW(siteUrl); + siteUrl = CoreUrl.addOrRemoveWWW(siteUrl); return this.getUserToken(siteUrl, username, password, service, true); } @@ -1697,7 +1697,7 @@ export class CoreSitesProvider { // Check if URL has http(s) protocol. if (!url.match(/^https?:\/\//i)) { // URL doesn't have http(s) protocol. Check if it has any protocol. - if (CoreUrlUtils.isAbsoluteURL(url)) { + if (CoreUrl.isAbsoluteURL(url)) { // It has some protocol. Return empty array. return []; } @@ -1943,11 +1943,13 @@ export class CoreSitesProvider { const site = await this.getSite(siteIds[0]); const siteUrl = CoreText.removeEndingSlash( - CoreUrlUtils.removeProtocolAndWWW(site.getURL()), + CoreUrl.removeUrlParts(site.getURL(), [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain]), + ); + const treatedUrl = CoreText.removeEndingSlash( + CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain]), ); - const treatedUrl = CoreText.removeEndingSlash(CoreUrlUtils.removeProtocolAndWWW(url)); - if (siteUrl == treatedUrl) { + if (siteUrl === treatedUrl) { result.site = site; } diff --git a/src/core/services/tests/utils/url.test.ts b/src/core/services/tests/utils/url.test.ts deleted file mode 100644 index fe3ee07b3..000000000 --- a/src/core/services/tests/utils/url.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -// (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 { CoreUrlUtilsProvider } from '@services/utils/url'; - -describe('CoreUrlUtilsProvider', () => { - - let urlUtils: CoreUrlUtilsProvider; - - beforeEach(() => { - urlUtils = new CoreUrlUtilsProvider(); - }); - - it('adds www if missing', () => { - const originalUrl = 'https://moodle.org'; - const url = urlUtils.addOrRemoveWWW(originalUrl); - - expect(url).toEqual('https://www.moodle.org'); - }); - - it('removes www if present', () => { - const originalUrl = 'https://www.moodle.org'; - const url = urlUtils.addOrRemoveWWW(originalUrl); - - expect(url).toEqual('https://moodle.org'); - }); - - it('adds params to URL without params', () => { - const originalUrl = 'https://moodle.org'; - const params = { - first: '1', - second: '2', - }; - const url = urlUtils.addParamsToUrl(originalUrl, params); - - expect(url).toEqual('https://moodle.org?first=1&second=2'); - }); - - it('adds params to URL with existing params', () => { - const originalUrl = 'https://moodle.org?existing=1'; - const params = { - first: '1', - second: '2', - }; - const url = urlUtils.addParamsToUrl(originalUrl, params); - - expect(url).toEqual('https://moodle.org?existing=1&first=1&second=2'); - }); - - it('doesn\'t change URL if no params supplied', () => { - const originalUrl = 'https://moodle.org'; - const url = urlUtils.addParamsToUrl(originalUrl); - - expect(url).toEqual(originalUrl); - }); - - it('doesn\'t add undefined or null params', () => { - const originalUrl = 'https://moodle.org'; - const url = urlUtils.addParamsToUrl(originalUrl, { - foo: undefined, - bar: null, - baz: 1, - }); - - expect(url).toEqual('https://moodle.org?baz=1'); - }); - - it('adds anchor to URL', () => { - const originalUrl = 'https://moodle.org'; - const params = { - first: '1', - second: '2', - }; - const url = urlUtils.addParamsToUrl(originalUrl, params, 'myanchor'); - - expect(url).toEqual('https://moodle.org?first=1&second=2#myanchor'); - }); - -}); diff --git a/src/core/services/urlschemes.ts b/src/core/services/urlschemes.ts index 780739f3e..b70a5542a 100644 --- a/src/core/services/urlschemes.ts +++ b/src/core/services/urlschemes.ts @@ -28,7 +28,7 @@ import { CoreNavigator, CoreRedirectPayload } from './navigator'; import { CoreSiteCheckResponse, CoreSites } from './sites'; import { CoreDomUtils } from './utils/dom'; import { CoreTextErrorObject, CoreTextUtils } from './utils/text'; -import { CoreUrlUtils } from './utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from './utils/utils'; /* @@ -240,13 +240,13 @@ export class CoreCustomURLSchemesProvider { url = this.removeCustomURLScheme(url); // Detect if there's a user specified. - const username = CoreUrlUtils.getUsernameFromUrl(url); + const username = CoreUrl.getUsernameFromUrl(url); if (username) { url = url.replace(username + '@', ''); // Remove the username from the URL. } // Get the params of the URL. - const params = CoreUrlUtils.extractUrlParams(url); + const params = CoreUrl.extractUrlParams(url); // Remove the params to get the site URL. if (url.indexOf('?') != -1) { @@ -293,7 +293,7 @@ export class CoreCustomURLSchemesProvider { url = this.removeCustomURLLinkScheme(url); // Detect if there's a user specified. - const username = CoreUrlUtils.getUsernameFromUrl(url); + const username = CoreUrl.getUsernameFromUrl(url); if (username) { url = url.replace(username + '@', ''); // Remove the username from the URL. } diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 5f238e435..c00e2f88d 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -21,7 +21,7 @@ import { CoreConfig } from '@services/config'; import { CoreFile } from '@services/file'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreTextUtils, CoreTextErrorObject } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { CoreIonLoadingElement } from '@classes/ion-loading'; @@ -710,7 +710,10 @@ export class CoreDomUtilsProvider { media.forEach((media: HTMLElement) => { const currentSrc = media.getAttribute('src'); const newSrc = currentSrc ? - paths[CoreUrlUtils.removeUrlParams(CoreTextUtils.decodeURIComponent(currentSrc))] : + paths[CoreUrl.removeUrlParts( + CoreTextUtils.decodeURIComponent(currentSrc), + [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment], + )] : undefined; if (newSrc !== undefined) { @@ -732,7 +735,10 @@ export class CoreDomUtilsProvider { anchors.forEach((anchor: HTMLElement) => { const currentHref = anchor.getAttribute('href'); const newHref = currentHref ? - paths[CoreUrlUtils.removeUrlParams(CoreTextUtils.decodeURIComponent(currentHref))] : + paths[CoreUrl.removeUrlParts( + CoreTextUtils.decodeURIComponent(currentHref), + [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment], + )] : undefined; if (newHref !== undefined) { diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts index 93e9a0933..39975ce21 100644 --- a/src/core/services/utils/iframe.ts +++ b/src/core/services/utils/iframe.ts @@ -21,12 +21,11 @@ import { CoreFile } from '@services/file'; import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton, NgZone, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; -import { CoreUrl } from '@singletons/url'; import { CoreWindow } from '@singletons/window'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CorePath } from '@singletons/path'; @@ -68,7 +67,7 @@ export class CoreIframeUtilsProvider { checkOnlineFrameInOffline(element: CoreFrameElement, isSubframe?: boolean): boolean { const src = 'src' in element ? element.src : element.data; - if (src && src != 'about:blank' && !CoreUrlUtils.isLocalFileUrl(src) && !CoreNetwork.isOnline()) { + if (src && src != 'about:blank' && !CoreUrl.isLocalFileUrl(src) && !CoreNetwork.isOnline()) { if (element.classList.contains('core-iframe-offline-disabled')) { // Iframe already hidden, stop. return true; @@ -234,7 +233,7 @@ export class CoreIframeUtilsProvider { */ getContentWindowAndDocument(element: CoreFrameElement): { window: Window | null; document: Document | null } { const src = 'src' in element ? element.src : element.data; - if (src !== 'about:blank' && !CoreUrlUtils.isLocalFileUrl(src)) { + if (src !== 'about:blank' && !CoreUrl.isLocalFileUrl(src)) { // No permissions to access the iframe. return { window: null, document: null }; } @@ -423,7 +422,7 @@ export class CoreIframeUtilsProvider { * @returns Promise resolved when done. */ protected async windowOpen(url: string, name: string, element?: CoreFrameElement): Promise { - const scheme = CoreUrlUtils.getUrlScheme(url); + const scheme = CoreUrl.getUrlProtocol(url); if (!scheme) { // It's a relative URL, use the frame src to create the full URL. const src = element @@ -488,12 +487,12 @@ export class CoreIframeUtilsProvider { const urlParts = CoreUrl.parse(link.href); const originalHref = 'getAttribute' in link ? link.getAttribute('href') : link.originalHref; - if (!link.href || !originalHref || originalHref == '#' || !urlParts || urlParts.protocol == 'javascript') { + if (!link.href || !originalHref || originalHref == '#' || !urlParts || urlParts.protocol === 'javascript') { // Links with no URL and Javascript links are ignored. return; } - if (urlParts.protocol && !CoreUrlUtils.isLocalFileUrlScheme(urlParts.protocol, urlParts.domain || '')) { + if (urlParts.protocol && !CoreUrl.isLocalFileUrlScheme(urlParts.protocol, urlParts.domain || '')) { // Scheme suggests it's an external resource. event && event.preventDefault(); @@ -503,7 +502,7 @@ export class CoreIframeUtilsProvider { if ( element && frameSrc && - !CoreUrlUtils.isLocalFileUrl(frameSrc) && + !CoreUrl.isLocalFileUrl(frameSrc) && (!link.target || link.target == '_self') ) { // Load the link inside the frame itself. @@ -574,7 +573,7 @@ export class CoreIframeUtilsProvider { * @returns Promise resolved when done. */ async fixIframeCookies(url: string): Promise { - if (!CorePlatform.isIOS() || !url || CoreUrlUtils.isLocalFileUrl(url)) { + if (!CorePlatform.isIOS() || !url || CoreUrl.isLocalFileUrl(url)) { // No need to fix cookies. return; } @@ -613,7 +612,7 @@ export class CoreIframeUtilsProvider { * @returns Boolean. */ shouldDisplayHelpForUrl(url: string): boolean { - return this.shouldDisplayHelp() && !CoreUrlUtils.isLocalFileUrl(url); + return this.shouldDisplayHelp() && !CoreUrl.isLocalFileUrl(url); } /** diff --git a/src/core/services/utils/url.ts b/src/core/services/utils/url.ts index e05a8725b..d00d4e62e 100644 --- a/src/core/services/utils/url.ts +++ b/src/core/services/utils/url.ts @@ -14,18 +14,12 @@ import { Injectable } from '@angular/core'; -import { CoreLang, CoreLangFormat } from '@services/lang'; -import { CoreTextUtils } from '@services/utils/text'; -import { CoreConstants } from '@/core/constants'; import { makeSingleton } from '@singletons'; -import { CoreUrl } from '@singletons/url'; -import { CoreSites } from '@services/sites'; -import { CorePath } from '@singletons/path'; -import { CorePlatform } from '@services/platform'; -import { CoreMedia } from '@singletons/media'; +import { CoreUrl, CoreUrlParams as CoreUrlParamsNew, CoreUrlPartNames } from '@singletons/url'; /* * "Utils" service with helper functions for URLs. + * @deprecated since 4.5. Use CoreUrl instead. */ @Injectable({ providedIn: 'root' }) export class CoreUrlUtilsProvider { @@ -35,19 +29,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to modify. * @returns Modified URL. + * @deprecated since 4.5. Use CoreUrl.addOrRemoveWWW instead. */ addOrRemoveWWW(url: string): string { - if (url) { - if (url.match(/http(s)?:\/\/www\./)) { - // Already has www. Remove it. - url = url.replace('www.', ''); - } else { - url = url.replace('https://', 'https://www.'); - url = url.replace('http://', 'http://www.'); - } - } - - return url; + return CoreUrl.addOrRemoveWWW(url); } /** @@ -58,43 +43,10 @@ export class CoreUrlUtilsProvider { * @param anchor Anchor text if needed. * @param boolToNumber Whether to convert bools to 1 or 0. * @returns URL with params. + * @deprecated since 4.5. Use CoreUrl.addParamsToUrl instead. */ addParamsToUrl(url: string, params?: Record, anchor?: string, boolToNumber?: boolean): string { - // Remove any existing anchor to add the params before it. - const urlAndAnchor = url.split('#'); - url = urlAndAnchor[0]; - - let separator = url.indexOf('?') !== -1 ? '&' : '?'; - - for (const key in params) { - let value = params[key]; - - if (boolToNumber && typeof value === 'boolean') { - // Convert booleans to 1 or 0. - value = value ? '1' : '0'; - } - - // Ignore objects and undefined. - if (typeof value !== 'object' && value !== undefined) { - url += separator + key + '=' + value; - separator = '&'; - } - } - - // Re-add the anchor if any. - if (urlAndAnchor.length > 1) { - // Remove the URL from the array. - urlAndAnchor.shift(); - - // Use a join in case there is more than one #. - url += '#' + urlAndAnchor.join('#'); - } - - if (anchor) { - url += '#' + anchor; - } - - return url; + return CoreUrl.addParamsToUrl(url, params, anchor, boolToNumber); } /** @@ -103,9 +55,10 @@ export class CoreUrlUtilsProvider { * @param url URL. * @param text Text of the link. * @returns Link. + * @deprecated since 4.5. Use CoreUrl.buildLink instead. */ buildLink(url: string, text: string): string { - return '' + text + ''; + return CoreUrl.buildLink(url, text); } /** @@ -115,14 +68,10 @@ export class CoreUrlUtilsProvider { * @param siteUrl The URL of the site the URL belongs to. * @param accessKey User access key for tokenpluginfile. * @returns Whether tokenpluginfile.php can be used. + * @deprecated since 4.5. Use CoreUrl.canUseTokenPluginFile instead. */ canUseTokenPluginFile(url: string, siteUrl: string, accessKey?: string): boolean { - // Do not use tokenpluginfile if site doesn't use slash params, the URL doesn't work. - // Also, only use it for "core" pluginfile endpoints. Some plugins can implement their own endpoint (like customcert). - return !CoreConstants.CONFIG.disableTokenFile && !!accessKey && !url.match(/[&?]file=/) && ( - url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0 || - url.indexOf(CorePath.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0) && - !CoreMedia.sourceUsesJavascriptPlayer({ src: url }); + return CoreUrl.canUseTokenPluginFile(url, siteUrl, accessKey); } /** @@ -130,43 +79,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to treat. * @returns Object with the params. + * @deprecated since 4.5. Use CoreUrl.extractUrlParams instead. */ - extractUrlParams(url: string): CoreUrlParams { - const regex = /[?&]+([^=&]+)=?([^&]*)?/gi; - const subParamsPlaceholder = '@@@SUBPARAMS@@@'; - const params: CoreUrlParams = {}; - const urlAndHash = url.split('#'); - const questionMarkSplit = urlAndHash[0].split('?'); - let subParams: string; - - if (questionMarkSplit.length > 2) { - // There is more than one question mark in the URL. This can happen if any of the params is a URL with params. - // We only want to treat the first level of params, so we'll remove this second list of params and restore it later. - questionMarkSplit.splice(0, 2); - - subParams = '?' + questionMarkSplit.join('?'); - urlAndHash[0] = urlAndHash[0].replace(subParams, subParamsPlaceholder); - } - - urlAndHash[0].replace(regex, (match: string, key: string, value: string): string => { - params[key] = value !== undefined ? CoreTextUtils.decodeURIComponent(value) : ''; - - if (subParams) { - params[key] = params[key].replace(subParamsPlaceholder, subParams); - } - - return match; - }); - - if (urlAndHash.length > 1) { - // Remove the URL from the array. - urlAndHash.shift(); - - // Add the hash as a param with a special name. Use a join in case there is more than one #. - params.urlHash = urlAndHash.join('#'); - } - - return params; + extractUrlParams(url: string): CoreUrlParamsNew { + return CoreUrl.extractUrlParams(url); } /** @@ -179,40 +95,10 @@ export class CoreUrlUtilsProvider { * @param siteUrl The URL of the site the URL belongs to. * @param accessKey User access key for tokenpluginfile. * @returns Fixed URL. + * @deprecated since 4.5. Use CoreUrl.fixPluginfileURL instead. */ fixPluginfileURL(url: string, token: string, siteUrl: string, accessKey?: string): string { - if (!url) { - return ''; - } - - url = url.replace(/&/g, '&'); - - const canUseTokenPluginFile = accessKey && this.canUseTokenPluginFile(url, siteUrl, accessKey); - - // First check if we need to fix this url or is already fixed. - if (!canUseTokenPluginFile && url.indexOf('token=') != -1) { - return url; - } - - // Check if is a valid URL (contains the pluginfile endpoint) and belongs to the site. - if (!this.isPluginFileUrl(url) || url.indexOf(CoreTextUtils.addEndingSlash(siteUrl)) !== 0) { - return url; - } - - if (canUseTokenPluginFile) { - // Use tokenpluginfile.php. - url = url.replace(/(\/webservice)?\/pluginfile\.php/, '/tokenpluginfile.php/' + accessKey); - } else { - // Use pluginfile.php. Some webservices returns directly the correct download url, others not. - if (url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0) { - url = url.replace('/pluginfile', '/webservice/pluginfile'); - } - - url = this.addParamsToUrl(url, { token }); - } - - // Always send offline=1 (it's for external repositories). - return this.addParamsToUrl(url, { offline: '1', lang: CoreLang.getCurrentLanguageSync(CoreLangFormat.LMS) }); + return CoreUrl.fixPluginfileURL(url, token, siteUrl, accessKey); } /** @@ -220,54 +106,26 @@ export class CoreUrlUtilsProvider { * * @param url The url to be formatted. * @returns Fromatted url. + * @deprecated since 4.5. Use CoreUrl.formatURL instead. */ formatURL(url: string): string { - url = url.trim(); - - // Check if the URL starts by http or https. - if (! /^http(s)?:\/\/.*/i.test(url)) { - // Test first allways https. - url = 'https://' + url; - } - - // http always in lowercase. - url = url.replace(/^http/i, 'http'); - url = url.replace(/^https/i, 'https'); - - // Replace last slash. - url = url.replace(/\/$/, ''); - - return url; + return CoreUrl.formatURL(url); } /** * Returns the URL to the documentation of the app, based on Moodle version and current language. * - * @param release Moodle release. + * The URL has been simplified and always returns the English version of the latest version of Moodle + * to simplify the circular dependencies. + * + * @param release Moodle release. Unused. * @param page Docs page to go to. * @returns Promise resolved with the Moodle docs URL. + * + * @deprecated since 4.5. You can use CoreAuthenticatedSite.getDocsUrl but is also deprecated. */ async getDocsUrl(release?: string, page: string = 'Mobile_app'): Promise { - let docsUrl = 'https://docs.moodle.org/en/' + page; - - if (release !== undefined) { - const version = CoreSites.getMajorReleaseNumber(release).replace('.', ''); - - // Check is a valid number. - if (Number(version) >= 24) { - // Append release number. - docsUrl = docsUrl.replace('https://docs.moodle.org/', 'https://docs.moodle.org/' + version + '/'); - } - } - - try { - let lang = await CoreLang.getCurrentLanguage(CoreLangFormat.LMS); - lang = CoreLang.getParentLanguage() || lang; - - return docsUrl.replace('/en/', '/' + lang + '/'); - } catch (error) { - return docsUrl; - } + return 'https://docs.moodle.org/en/' + page; } /** @@ -275,53 +133,10 @@ export class CoreUrlUtilsProvider { * * @param url URL * @returns Youtube Embed Video URL or undefined if not found. + * @deprecated since 4.5. Use CoreUrl.getYoutubeEmbedUrl instead. */ getYoutubeEmbedUrl(url?: string): string | void { - if (!url) { - return; - } - - let videoId = ''; - const params: CoreUrlParams = {}; - - url = CoreTextUtils.decodeHTML(url); - - // Get the video ID. - let match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/); - - if (match && match[2].length === 11) { - videoId = match[2]; - } - - // No videoId, do not continue. - if (!videoId) { - return; - } - - // Now get the playlist (if any). - match = url.match(/[?&]list=([^#&?]+)/); - - if (match && match[1]) { - params.list = match[1]; - } - - // Now get the start time (if any). - match = url.match(/[?&]start=(\d+)/); - - if (match && match[1]) { - params.start = parseInt(match[1], 10).toString(); - } else { - // No start param, but it could have a time param. - match = url.match(/[?&]t=(\d+h)?(\d+m)?(\d+s)?/); - if (match) { - const start = (match[1] ? parseInt(match[1], 10) * 3600 : 0) + - (match[2] ? parseInt(match[2], 10) * 60 : 0) + - (match[3] ? parseInt(match[3], 10) : 0); - params.start = start.toString(); - } - } - - return this.addParamsToUrl('https://www.youtube.com/embed/' + videoId, params); + return CoreUrl.getYoutubeEmbedUrl(url); } /** @@ -331,15 +146,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to treat. * @returns Last file without params. + * @deprecated since 4.5. Use CoreUrl.getLastFileWithoutParams instead. */ getLastFileWithoutParams(url: string): string { - const parsedUrl = CoreUrl.parse(url); - if (!parsedUrl) { - return ''; - } - const path = parsedUrl.path ?? ''; - - return path.split('/').pop() ?? ''; + return CoreUrl.getLastFileWithoutParams(url); } /** @@ -348,17 +158,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to treat. * @returns Protocol, undefined if no protocol found. - * @todo Use CoreUrl.parse + * @deprecated since 4.5. Use CoreUrl.getUrlProtocol instead. */ getUrlProtocol(url: string): string | void { - if (!url) { - return; - } - - const matches = url.match(/^([^/:.?]*):\/\//); - if (matches && matches[1]) { - return matches[1]; - } + return CoreUrl.getUrlProtocol(url); } /** @@ -367,36 +170,21 @@ export class CoreUrlUtilsProvider { * * @param url URL to treat. * @returns Scheme, undefined if no scheme found. + * @deprecated since 4.5. Use CoreUrl.getUrlProtocol instead. */ getUrlScheme(url: string): string | void { - if (!url) { - return; - } - - const matches = url.match(/^([a-z][a-z0-9+\-.]*):/); - if (matches && matches[1]) { - return matches[1]; - } + return CoreUrl.getUrlProtocol(url); } - /* + /** * Gets a username from a URL like: user@mysite.com. * * @param url URL to treat. * @returns Username. Undefined if no username found. - * @todo Use CoreUrl.parse + * @deprecated since 4.5. Use CoreUrl.getUsernameFromUrl instead. */ getUsernameFromUrl(url: string): string | undefined { - if (url.indexOf('@') > -1) { - // Get URL without protocol. - const withoutProtocol = url.replace(/^[^?@/]*:\/\//, ''); - const matches = withoutProtocol.match(/[^@]*/); - - // Make sure that @ is at the start of the URL, not in a param at the end. - if (matches && matches.length && !matches[0].match(/[/|?]/)) { - return matches[0]; - } - } + return CoreUrl.getUsernameFromUrl(url); } /** @@ -404,9 +192,10 @@ export class CoreUrlUtilsProvider { * * @param url The url to test against the pattern. * @returns Whether the url is absolute. + * @deprecated since 4.5. Use CoreUrl.isAbsoluteURL instead. */ isAbsoluteURL(url: string): boolean { - return /^[^:]{2,}:\/\//i.test(url) || /^(tel:|mailto:|geo:)/.test(url); + return CoreUrl.isAbsoluteURL(url); } /** @@ -414,9 +203,10 @@ export class CoreUrlUtilsProvider { * * @param url The URL to test. * @returns Whether the URL is downloadable. + * @deprecated since 4.5. Use CoreUrl.isDownloadableUrl instead. */ isDownloadableUrl(url: string): boolean { - return this.isPluginFileUrl(url) || this.isTokenPluginFileUrl(url) || this.isThemeImageUrl(url) || this.isGravatarUrl(url); + return CoreUrl.isDownloadableUrl(url); } /** @@ -424,9 +214,10 @@ export class CoreUrlUtilsProvider { * * @param url The URL to test. * @returns Whether the URL is a gravatar URL. + * @deprecated since 4.5. Use CoreUrl.isGravatarUrl instead. */ isGravatarUrl(url: string): boolean { - return url?.indexOf('gravatar.com/avatar') !== -1; + return CoreUrl.isGravatarUrl(url); } /** @@ -434,10 +225,10 @@ export class CoreUrlUtilsProvider { * * @param url The url to test. * @returns Whether the url uses http or https protocol. - * @todo Use CoreUrl.parse + * @deprecated since 4.5. Use CoreUrl.isHttpURL instead. */ isHttpURL(url: string): boolean { - return /^https?:\/\/.+/i.test(url); + return CoreUrl.isHttpURL(url); } /** @@ -445,11 +236,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to check. * @returns Whether the URL belongs to a local file. + * @deprecated since 4.5. Use CoreUrl.isLocalFileUrl instead. */ isLocalFileUrl(url: string): boolean { - const urlParts = CoreUrl.parse(url); - - return this.isLocalFileUrlScheme(urlParts?.protocol || '', urlParts?.domain || ''); + return CoreUrl.isLocalFileUrl(url); } /** @@ -457,18 +247,10 @@ export class CoreUrlUtilsProvider { * * @param scheme Scheme to check. * @returns Whether the scheme belongs to a local file. + * @deprecated since 4.5. Use CoreUrl.isLocalFileUrlScheme instead. */ isLocalFileUrlScheme(scheme: string, domain: string): boolean { - if (!scheme) { - return false; - } - scheme = scheme.toLowerCase(); - - return scheme == 'cdvfile' || - scheme == 'file' || - scheme == 'filesystem' || - scheme == CoreConstants.CONFIG.ioswebviewscheme || - (CorePlatform.isMobile() && scheme === 'http' && domain === 'localhost'); // @todo Get served domain from ENV. + return CoreUrl.isLocalFileUrlScheme(scheme, domain); } /** @@ -476,9 +258,10 @@ export class CoreUrlUtilsProvider { * * @param url The URL to test. * @returns Whether the URL is a pluginfile URL. + * @deprecated since 4.5. Use CoreUrl.isPluginFileUrl instead. */ isPluginFileUrl(url: string): boolean { - return url.indexOf('/pluginfile.php') !== -1; + return CoreUrl.isPluginFileUrl(url); } /** @@ -486,9 +269,10 @@ export class CoreUrlUtilsProvider { * * @param url The URL to test. * @returns Whether the URL is a tokenpluginfile URL. + * @deprecated since 4.5. Use CoreUrl.isTokenPluginFileUrl instead. */ isTokenPluginFileUrl(url: string): boolean { - return url.indexOf('/tokenpluginfile.php') !== -1; + return CoreUrl.isTokenPluginFileUrl(url); } /** @@ -497,13 +281,10 @@ export class CoreUrlUtilsProvider { * @param imageUrl The URL to test. * @param siteUrl The Site Url. * @returns Whether the URL is a theme image URL. + * @deprecated since 4.5. Use CoreUrl.isThemeImageUrl instead. */ isThemeImageUrl(imageUrl: string, siteUrl?: string): boolean { - if (siteUrl) { - return imageUrl.startsWith(`${siteUrl}/theme/image.php`); - } - - return imageUrl?.indexOf('/theme/image.php') !== -1; + return CoreUrl.isThemeImageUrl(imageUrl, siteUrl); } /** @@ -513,57 +294,10 @@ export class CoreUrlUtilsProvider { * @param param Param to get from the URL. * @param siteUrl Site URL. * @returns Param from the URL. + * @deprecated since 4.5. Use CoreUrl.getThemeImageUrlParam instead. */ getThemeImageUrlParam(imageUrl: string, param: string, siteUrl?: string): string { - if (!this.isThemeImageUrl(imageUrl, siteUrl)) { - // Cannot be guessed. - return ''; - } - - const matches = imageUrl.match('/theme/image.php/(.*)'); - if (matches?.[1]) { - // Slash arguments found. - const slasharguments = matches[1].split('/'); - - if (slasharguments.length < 4) { - // Image not found, malformed URL. - return ''; - } - - // Join from the third element to the end. - const image = slasharguments.slice(3).join('/'); - switch (param) { - case 'theme': - return slasharguments[0]; - case 'component': - return slasharguments[1]; - case 'rev': - return slasharguments[2]; - case 'image': - // Remove possible url params. - return CoreUrlUtils.removeUrlParams(image); - default: - return CoreUrlUtils.extractUrlParams(image)[param] || ''; - } - - } - - // URL arguments found. - const iconParams = CoreUrlUtils.extractUrlParams(imageUrl); - - switch (param) { - case 'theme': - return iconParams[param] || 'standard'; - case 'component': - return iconParams[param] || 'core'; - case 'rev': - return iconParams[param] || '-1'; - case 'svg': - return iconParams[param] || '1'; - case 'image': - default: - return iconParams[param] || ''; - } + return CoreUrl.getThemeImageUrlParam(imageUrl, param, siteUrl); } /** @@ -571,14 +305,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to treat. * @returns Treated URL. + * @deprecated since 4.5. Use CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain]) instead. */ removeProtocolAndWWW(url: string): string { - // Remove protocol. - url = url.replace(/^.*?:\/\//, ''); - // Remove www. - url = url.replace(/^www./, ''); - - return url; + return CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain]); } /** @@ -586,11 +316,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to treat. * @returns URL without params. + * @deprecated since 4.5. Use CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment]) instead. */ removeUrlParams(url: string): string { - const matches = url.match(/^[^?]+/); - - return matches ? matches[0] : ''; + return CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment]); } /** @@ -599,33 +328,16 @@ export class CoreUrlUtilsProvider { * @param url The url to be fixed. * @param siteUrl The URL of the site the URL belongs to. * @returns Modified URL. + * @deprecated since 4.5. Use CoreUrl.unfixPluginfileURL instead. */ unfixPluginfileURL(url: string, siteUrl?: string): string { - if (!url) { - return ''; - } - - url = url.replace(/&/g, '&'); - - // It site URL is supplied, check if the URL belongs to the site. - if (siteUrl && url.indexOf(CoreTextUtils.addEndingSlash(siteUrl)) !== 0) { - return url; - } - - // Check tokenpluginfile first. - url = url.replace(/\/tokenpluginfile\.php\/[^/]+\//, '/pluginfile.php/'); - - // Treat webservice/pluginfile case. - url = url.replace(/\/webservice\/pluginfile\.php\//, '/pluginfile.php/'); - - // Make sure the URL doesn't contain the token. - url = url.replace(/([?&])token=[^&]*&?/, '$1'); - - return url; + return CoreUrl.unfixPluginfileURL(url, siteUrl); } } - export const CoreUrlUtils = makeSingleton(CoreUrlUtilsProvider); -export type CoreUrlParams = {[key: string]: string}; +/** + * @deprecated since 4.5. Use CoreUrlParams on CoreUrl instead. + */ +export type CoreUrlParams = CoreUrlParamsNew; diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index ea2b2e1f4..cdced27db 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -37,7 +37,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from './url'; +import { CoreUrl } from '@singletons/url'; import { QRScanner } from '@features/native/plugins'; import { CoreArray } from '@singletons/array'; import { CoreText } from '@singletons/text'; @@ -1114,7 +1114,7 @@ export class CoreUtilsProvider { CoreAnalytics.logEvent({ type: CoreAnalyticsEventType.OPEN_LINK, - link: CoreUrlUtils.unfixPluginfileURL(options.originalUrl ?? url), + link: CoreUrl.unfixPluginfileURL(options.originalUrl ?? url), }); return this.iabInstance; @@ -1172,7 +1172,7 @@ export class CoreUtilsProvider { */ async openInBrowser(url: string, options: CoreUtilsOpenInBrowserOptions = {}): Promise { // eslint-disable-next-line deprecation/deprecation - const originaUrl = CoreUrlUtils.unfixPluginfileURL(options.originalUrl ?? options.browserWarningUrl ?? url); + const originaUrl = CoreUrl.unfixPluginfileURL(options.originalUrl ?? options.browserWarningUrl ?? url); if (options.showBrowserWarning || options.showBrowserWarning === undefined) { try { await CoreWindow.confirmOpenBrowserIfNeeded(originaUrl); @@ -1217,7 +1217,7 @@ export class CoreUtilsProvider { CoreAnalytics.logEvent({ type: CoreAnalyticsEventType.OPEN_LINK, - link: CoreUrlUtils.unfixPluginfileURL(url), + link: CoreUrl.unfixPluginfileURL(url), }); return; diff --git a/src/core/singletons/tests/url.test.ts b/src/core/singletons/tests/url.test.ts index 228fb7cf8..08410360b 100644 --- a/src/core/singletons/tests/url.test.ts +++ b/src/core/singletons/tests/url.test.ts @@ -14,10 +14,75 @@ import { mock } from '@/testing/utils'; import { CoreSite } from '@classes/sites/site'; -import { CoreUrl } from '@singletons/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; describe('CoreUrl singleton', () => { + it('adds www if missing', () => { + const originalUrl = 'https://moodle.org'; + const url = CoreUrl.addOrRemoveWWW(originalUrl); + + expect(url).toEqual('https://www.moodle.org'); + }); + + it('removes www if present', () => { + const originalUrl = 'https://www.moodle.org'; + const url = CoreUrl.addOrRemoveWWW(originalUrl); + + expect(url).toEqual('https://moodle.org'); + }); + + it('adds params to URL without params', () => { + const originalUrl = 'https://moodle.org'; + const params = { + first: '1', + second: '2', + }; + const url = CoreUrl.addParamsToUrl(originalUrl, params); + + expect(url).toEqual('https://moodle.org?first=1&second=2'); + }); + + it('adds params to URL with existing params', () => { + const originalUrl = 'https://moodle.org?existing=1'; + const params = { + first: '1', + second: '2', + }; + const url = CoreUrl.addParamsToUrl(originalUrl, params); + + expect(url).toEqual('https://moodle.org?existing=1&first=1&second=2'); + }); + + it('doesn\'t change URL if no params supplied', () => { + const originalUrl = 'https://moodle.org'; + const url = CoreUrl.addParamsToUrl(originalUrl); + + expect(url).toEqual(originalUrl); + }); + + it('doesn\'t add undefined or null params', () => { + const originalUrl = 'https://moodle.org'; + const url = CoreUrl.addParamsToUrl(originalUrl, { + foo: undefined, + bar: null, + baz: 1, + }); + + expect(url).toEqual('https://moodle.org?baz=1'); + }); + + it('adds anchor to URL', () => { + const originalUrl = 'https://moodle.org'; + const params = { + first: '1', + second: '2', + }; + const url = CoreUrl.addParamsToUrl(originalUrl, params, 'myanchor'); + + expect(url).toEqual('https://moodle.org?first=1&second=2#myanchor'); + }); + it('parses standard urls', () => { expect(CoreUrl.parse('https://u1:pw1@my.subdomain.com/path/?query=search#hash')).toEqual({ protocol: 'https', @@ -83,9 +148,34 @@ describe('CoreUrl singleton', () => { }); it('removes protocol', () => { - expect(CoreUrl.removeProtocol('https://school.edu')).toEqual('school.edu'); - expect(CoreUrl.removeProtocol('ftp://school.edu')).toEqual('school.edu'); - expect(CoreUrl.removeProtocol('school.edu')).toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('https://school.edu', CoreUrlPartNames.Protocol)).toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('ftp://school.edu', CoreUrlPartNames.Protocol)).toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('school.edu', CoreUrlPartNames.Protocol)).toEqual('school.edu'); + }); + + it('removes protocol and www', () => { + expect(CoreUrl.removeUrlParts('https://www.school.edu', [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain])) + .toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('ftp://school.edu', [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain])) + .toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('www.school.edu', [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain])) + .toEqual('school.edu'); + // Test that it works in a different order. + expect(CoreUrl.removeUrlParts('https://www.school.edu', [CoreUrlPartNames.WWWInDomain, CoreUrlPartNames.Protocol])) + .toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('ftp://school.edu', [CoreUrlPartNames.WWWInDomain, CoreUrlPartNames.Protocol])) + .toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('www.school.edu', [CoreUrlPartNames.WWWInDomain, CoreUrlPartNames.Protocol])) + .toEqual('school.edu'); + }); + + it('removes params', () => { + expect(CoreUrl.removeUrlParts('https://www.school.edu?blabla#a', [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment])) + .toEqual('https://www.school.edu'); + expect(CoreUrl.removeUrlParts('ftp://school.edu?blabla=r#a', [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment])) + .toEqual('ftp://school.edu'); + expect(CoreUrl.removeUrlParts('www.school.edu?blabla=5&gg=3', [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment])) + .toEqual('www.school.edu'); }); it('compares domains and paths', () => { @@ -108,9 +198,21 @@ describe('CoreUrl singleton', () => { }); it('removes the anchor of a URL', () => { - expect(CoreUrl.removeUrlAnchor('https://school.edu#foo')).toEqual('https://school.edu'); - expect(CoreUrl.removeUrlAnchor('https://school.edu#foo#bar')).toEqual('https://school.edu'); - expect(CoreUrl.removeUrlAnchor('https://school.edu')).toEqual('https://school.edu'); + expect(CoreUrl.removeUrlParts('https://school.edu#foo', CoreUrlPartNames.Fragment)).toEqual('https://school.edu'); + expect(CoreUrl.removeUrlParts('https://school.edu#foo#bar', CoreUrlPartNames.Fragment)).toEqual('https://school.edu'); + expect(CoreUrl.removeUrlParts('https://school.edu', CoreUrlPartNames.Fragment)).toEqual('https://school.edu'); + }); + + it('gets the username from a URL', () => { + expect(CoreUrl.getUsernameFromUrl( + 'https://username@domain.com?token=TOKEN&privatetoken=PRIVATETOKEN&redirect=http://domain.com/course/view.php?id=2', + )).toEqual('username'); + expect(CoreUrl.getUsernameFromUrl( + 'https://username:password@domain.com?token=TOKEN&privatetoken=PRIVATETOKEN&redirect=http://domain.com/course/', + )).toEqual('username'); + expect(CoreUrl.getUsernameFromUrl( + 'https://domain.com?token=TOKEN&privatetoken=PRIVATETOKEN&redirect=http://domain.com/course/view.php?id=2', + )).toEqual(undefined); }); it('converts to absolute URLs', () => { diff --git a/src/core/singletons/url.ts b/src/core/singletons/url.ts index dea4022c1..d033268dc 100644 --- a/src/core/singletons/url.ts +++ b/src/core/singletons/url.ts @@ -16,6 +16,12 @@ import { CoreSite } from '@classes/sites/site'; import { CorePath } from './path'; import { CoreText } from './text'; +import { CorePlatform } from '@services/platform'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreConstants } from '../constants'; +import { CoreMedia } from './media'; +import { CoreLang, CoreLangFormat } from '@services/lang'; + /** * Parts contained within a url. */ @@ -68,6 +74,13 @@ interface UrlParts { } +export const enum CoreUrlPartNames { + Protocol = 'protocol', + WWWInDomain = 'www', // Will remove starting www from domain. + Query = 'query', + Fragment = 'fragment', +} + /** * Singleton with helper functions for urls. */ @@ -85,8 +98,9 @@ export class CoreUrl { * @returns Url parts. */ static parse(url: string): UrlParts | null { + url = url.trim(); // Parse url with regular expression taken from RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B. - const match = url.trim().match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/); + const match = url.match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/); if (!match) { return null; @@ -120,8 +134,12 @@ export class CoreUrl { * @returns Assembled URL. */ static assemble(parts: UrlParts): string { - return (parts.protocol ? `${parts.protocol}://` : '') + - (parts.credentials ? `${parts.credentials}@` : '') + + const protocol = parts.protocol; + const credentials = parts.credentials || + (parts.password ? `${parts.username}:${parts.password}` : parts.username); + + return (protocol ? `${protocol}://` : '') + + (credentials ? `${credentials}@` : '') + (parts.domain ?? '') + (parts.port ? `:${parts.port}` : '') + (parts.path ?? '') + @@ -190,9 +208,10 @@ export class CoreUrl { * * @param url Site url. * @returns Url without protocol. + * @deprecated since 4.5. Use CoreUrl.removeUrlParts(url, CoreUrlPartNames.Protocol) instead. */ static removeProtocol(url: string): string { - return url.replace(/^[a-zA-Z]+:\/\//i, ''); + return CoreUrl.removeUrlParts(url, CoreUrlPartNames.Protocol); } /** @@ -229,12 +248,9 @@ export class CoreUrl { * @returns Anchor, undefined if no anchor. */ static getUrlAnchor(url: string): string | undefined { - const firstAnchorIndex = url.indexOf('#'); - if (firstAnchorIndex === -1) { - return; - } + const urlParts = CoreUrl.parse(url); - return url.substring(firstAnchorIndex); + return urlParts?.fragment ? `#${urlParts.fragment}` : undefined; } /** @@ -242,11 +258,11 @@ export class CoreUrl { * * @param url URL. * @returns URL without anchor if any. + * + * @deprecated since 4.5. Use CoreUrl.removeUrlParts(url, CoreUrlPartNames.Fragment) instead. */ static removeUrlAnchor(url: string): string { - const urlAndAnchor = url.split('#'); - - return urlAndAnchor[0]; + return CoreUrl.removeUrlParts(url, CoreUrlPartNames.Fragment); } /** @@ -290,13 +306,13 @@ export class CoreUrl { * @returns Relative URL. */ static toRelativeURL(parentUrl: string, url: string): string { - parentUrl = CoreUrl.removeProtocol(parentUrl); + parentUrl = CoreUrl.removeUrlParts(parentUrl, CoreUrlPartNames.Protocol); if (!url.includes(parentUrl)) { return url; // Already relative URL. } - return CoreText.removeStartingSlash(CoreUrl.removeProtocol(url).replace(parentUrl, '')); + return CoreText.removeStartingSlash(CoreUrl.removeUrlParts(url, CoreUrlPartNames.Protocol).replace(parentUrl, '')); } /** @@ -344,4 +360,564 @@ export class CoreUrl { return newUrl; } + /** + * Add or remove 'www' from a URL. The url needs to have http or https protocol. + * + * @param url URL to modify. + * @returns Modified URL. + */ + static addOrRemoveWWW(url: string): string { + if (url) { + if (url.match(/http(s)?:\/\/www\./)) { + // Already has www. Remove it. + url = url.replace('www.', ''); + } else { + url = url.replace('https://', 'https://www.'); + url = url.replace('http://', 'http://www.'); + } + } + + return url; + } + + /** + * Add params to a URL. + * + * @param url URL to add the params to. + * @param params Object with the params to add. + * @param anchor Anchor text if needed. + * @param boolToNumber Whether to convert bools to 1 or 0. + * @returns URL with params. + */ + static addParamsToUrl(url: string, params?: Record, anchor?: string, boolToNumber?: boolean): string { + // Remove any existing anchor to add the params before it. + const urlAndAnchor = url.split('#'); + url = urlAndAnchor[0]; + + let separator = url.indexOf('?') !== -1 ? '&' : '?'; + + for (const key in params) { + let value = params[key]; + + if (boolToNumber && typeof value === 'boolean') { + // Convert booleans to 1 or 0. + value = value ? '1' : '0'; + } + + // Ignore objects and undefined. + if (typeof value !== 'object' && value !== undefined) { + url += separator + key + '=' + value; + separator = '&'; + } + } + + // Re-add the anchor if any. + if (urlAndAnchor.length > 1) { + // Remove the URL from the array. + urlAndAnchor.shift(); + + // Use a join in case there is more than one #. + url += '#' + urlAndAnchor.join('#'); + } + + if (anchor) { + url += '#' + anchor; + } + + return url; + } + + /** + * Given a URL and a text, return an HTML link. + * + * @param url URL. + * @param text Text of the link. + * @returns Link. + */ + static buildLink(url: string, text: string): string { + return '' + text + ''; + } + + /** + * Check whether we can use tokenpluginfile.php endpoint for a certain URL. + * + * @param url URL to check. + * @param siteUrl The URL of the site the URL belongs to. + * @param accessKey User access key for tokenpluginfile. + * @returns Whether tokenpluginfile.php can be used. + */ + static canUseTokenPluginFile(url: string, siteUrl: string, accessKey?: string): boolean { + // Do not use tokenpluginfile if site doesn't use slash params, the URL doesn't work. + // Also, only use it for "core" pluginfile endpoints. Some plugins can implement their own endpoint (like customcert). + return !CoreConstants.CONFIG.disableTokenFile && !!accessKey && !url.match(/[&?]file=/) && ( + url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0 || + url.indexOf(CorePath.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0) && + !CoreMedia.sourceUsesJavascriptPlayer({ src: url }); + } + + /** + * Extracts the parameters from a URL and stores them in an object. + * + * @param url URL to treat. + * @returns Object with the params. + */ + static extractUrlParams(url: string): CoreUrlParams { + const regex = /[?&]+([^=&]+)=?([^&]*)?/gi; + const subParamsPlaceholder = '@@@SUBPARAMS@@@'; + const params: CoreUrlParams = {}; + const urlAndHash = url.split('#'); + const questionMarkSplit = urlAndHash[0].split('?'); + let subParams: string; + + if (questionMarkSplit.length > 2) { + // There is more than one question mark in the URL. This can happen if any of the params is a URL with params. + // We only want to treat the first level of params, so we'll remove this second list of params and restore it later. + questionMarkSplit.splice(0, 2); + + subParams = '?' + questionMarkSplit.join('?'); + urlAndHash[0] = urlAndHash[0].replace(subParams, subParamsPlaceholder); + } + + urlAndHash[0].replace(regex, (match: string, key: string, value: string): string => { + params[key] = value !== undefined ? CoreTextUtils.decodeURIComponent(value) : ''; + + if (subParams) { + params[key] = params[key].replace(subParamsPlaceholder, subParams); + } + + return match; + }); + + if (urlAndHash.length > 1) { + // Remove the URL from the array. + urlAndHash.shift(); + + // Add the hash as a param with a special name. Use a join in case there is more than one #. + params.urlHash = urlAndHash.join('#'); + } + + return params; + } + + /** + * Generic function for adding the wstoken to Moodle urls and for pointing to the correct script. + * For download remote files from Moodle we need to use the special /webservice/pluginfile passing + * the ws token as a get parameter. + * + * @param url The url to be fixed. + * @param token Token to use. + * @param siteUrl The URL of the site the URL belongs to. + * @param accessKey User access key for tokenpluginfile. + * @returns Fixed URL. + */ + static fixPluginfileURL(url: string, token: string, siteUrl: string, accessKey?: string): string { + if (!url) { + return ''; + } + + url = url.replace(/&/g, '&'); + + const canUseTokenPluginFile = accessKey && CoreUrl.canUseTokenPluginFile(url, siteUrl, accessKey); + + // First check if we need to fix this url or is already fixed. + if (!canUseTokenPluginFile && url.indexOf('token=') != -1) { + return url; + } + + // Check if is a valid URL (contains the pluginfile endpoint) and belongs to the site. + if (!CoreUrl.isPluginFileUrl(url) || url.indexOf(CoreTextUtils.addEndingSlash(siteUrl)) !== 0) { + return url; + } + + if (canUseTokenPluginFile) { + // Use tokenpluginfile.php. + url = url.replace(/(\/webservice)?\/pluginfile\.php/, '/tokenpluginfile.php/' + accessKey); + } else { + // Use pluginfile.php. Some webservices returns directly the correct download url, others not. + if (url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0) { + url = url.replace('/pluginfile', '/webservice/pluginfile'); + } + + url = CoreUrl.addParamsToUrl(url, { token }); + } + + // Always send offline=1 (it's for external repositories). + return CoreUrl.addParamsToUrl(url, { offline: '1', lang: CoreLang.getCurrentLanguageSync(CoreLangFormat.LMS) }); + } + + /** + * Formats a URL, trim, lowercase, etc... + * + * @param url The url to be formatted. + * @returns Fromatted url. + */ + static formatURL(url: string): string { + url = url.trim(); + + // Check if the URL starts by http or https. + if (! /^http(s)?:\/\/.*/i.test(url)) { + // Test first allways https. + url = 'https://' + url; + } + + // http always in lowercase. + url = url.replace(/^http/i, 'http'); + url = url.replace(/^https/i, 'https'); + + // Replace last slash. + url = url.replace(/\/$/, ''); + + return url; + } + + /** + * Returns the Youtube Embed Video URL or undefined if not found. + * + * @param url URL + * @returns Youtube Embed Video URL or undefined if not found. + */ + static getYoutubeEmbedUrl(url?: string): string | void { + if (!url) { + return; + } + + let videoId = ''; + const params: CoreUrlParams = {}; + + url = CoreTextUtils.decodeHTML(url); + + // Get the video ID. + let match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/); + + if (match && match[2].length === 11) { + videoId = match[2]; + } + + // No videoId, do not continue. + if (!videoId) { + return; + } + + // Now get the playlist (if any). + match = url.match(/[?&]list=([^#&?]+)/); + + if (match && match[1]) { + params.list = match[1]; + } + + // Now get the start time (if any). + match = url.match(/[?&]start=(\d+)/); + + if (match && match[1]) { + params.start = parseInt(match[1], 10).toString(); + } else { + // No start param, but it could have a time param. + match = url.match(/[?&]t=(\d+h)?(\d+m)?(\d+s)?/); + if (match) { + const start = (match[1] ? parseInt(match[1], 10) * 3600 : 0) + + (match[2] ? parseInt(match[2], 10) * 60 : 0) + + (match[3] ? parseInt(match[3], 10) : 0); + params.start = start.toString(); + } + } + + return CoreUrl.addParamsToUrl('https://www.youtube.com/embed/' + videoId, params); + } + + /** + * Given a URL, returns what's after the last '/' without params. + * Example: + * http://mysite.com/a/course.html?id=1 -> course.html + * + * @param url URL to treat. + * @returns Last file without params. + */ + static getLastFileWithoutParams(url: string): string { + const parsedUrl = CoreUrl.parse(url); + if (!parsedUrl) { + return ''; + } + const path = parsedUrl.path ?? ''; + + return path.split('/').pop() ?? ''; + } + + /** + * Get the protocol from a URL. + * E.g. http://www.google.com returns 'http'. + * + * @param url URL to treat. + * @returns Protocol, undefined if no protocol found. + */ + static getUrlProtocol(url: string): string | void { + return CoreUrl.parse(url)?.protocol; + } + + /** + * Gets a username from a URL like: user@mysite.com. + * + * @param url URL to treat. + * @returns Username. Undefined if no username found. + * @todo Use CoreUrl.parse. It cannot use it right now because it won't detect username on custom URL with double protocol. + */ + static getUsernameFromUrl(url: string): string | undefined { + if (url.indexOf('@') < 0) { + return; + } + + // Get URL without protocol. + const withoutProtocol = url.replace(/^[^?@/]*:\/\//, ''); + const matches = withoutProtocol.match(/[^@]*/); + + // Make sure that @ is at the start of the URL, not in a param at the end. + if (matches && matches.length && !matches[0].match(/[/|?]/)) { + const credentials = matches[0]; + + return credentials.split(':')[0]; + } + } + + /** + * Returns if a URL has any protocol (not a relative URL). + * + * @param url The url to test against the pattern. + * @returns Whether the url is absolute. + */ + static isAbsoluteURL(url: string): boolean { + return /^[^:]{2,}:\/\//i.test(url) || /^(tel:|mailto:|geo:)/.test(url); + } + + /** + * Returns if a URL is downloadable: plugin file OR theme/image.php OR gravatar. + * + * @param url The URL to test. + * @returns Whether the URL is downloadable. + */ + static isDownloadableUrl(url: string): boolean { + return CoreUrl.isPluginFileUrl(url) || + CoreUrl.isTokenPluginFileUrl(url) || + CoreUrl.isThemeImageUrl(url) || + CoreUrl.isGravatarUrl(url); + } + + /** + * Returns if a URL is a gravatar URL. + * + * @param url The URL to test. + * @returns Whether the URL is a gravatar URL. + */ + static isGravatarUrl(url: string): boolean { + return url?.indexOf('gravatar.com/avatar') !== -1; + } + + /** + * Check if a URL uses http or https protocol. + * + * @param url The url to test. + * @returns Whether the url uses http or https protocol. + * @todo Use CoreUrl.parse + */ + static isHttpURL(url: string): boolean { + return /^https?:\/\/.+/i.test(url); + } + + /** + * Check whether an URL belongs to a local file. + * + * @param url URL to check. + * @returns Whether the URL belongs to a local file. + */ + static isLocalFileUrl(url: string): boolean { + const urlParts = CoreUrl.parse(url); + + return CoreUrl.isLocalFileUrlScheme(urlParts?.protocol || '', urlParts?.domain || ''); + } + + /** + * Check whether a URL scheme belongs to a local file. + * + * @param scheme Scheme to check. + * @returns Whether the scheme belongs to a local file. + */ + static isLocalFileUrlScheme(scheme: string, domain: string): boolean { + if (!scheme) { + return false; + } + scheme = scheme.toLowerCase(); + + return scheme === 'cdvfile' || + scheme === 'file' || + scheme === 'filesystem' || + scheme === CoreConstants.CONFIG.ioswebviewscheme || + (CorePlatform.isMobile() && scheme === 'http' && domain === 'localhost'); // @todo Get served domain from ENV. + } + + /** + * Returns if a URL is a pluginfile URL. + * + * @param url The URL to test. + * @returns Whether the URL is a pluginfile URL. + */ + static isPluginFileUrl(url: string): boolean { + return url.indexOf('/pluginfile.php') !== -1; + } + + /** + * Returns if a URL is a tokenpluginfile URL. + * + * @param url The URL to test. + * @returns Whether the URL is a tokenpluginfile URL. + */ + static isTokenPluginFileUrl(url: string): boolean { + return url.indexOf('/tokenpluginfile.php') !== -1; + } + + /** + * Returns if a URL is a theme image URL. + * + * @param imageUrl The URL to test. + * @param siteUrl The Site Url. + * @returns Whether the URL is a theme image URL. + */ + static isThemeImageUrl(imageUrl: string, siteUrl?: string): boolean { + if (siteUrl) { + return imageUrl.startsWith(`${siteUrl}/theme/image.php`); + } + + return imageUrl?.indexOf('/theme/image.php') !== -1; + } + + /** + * Returns an specific param from an image URL. + * + * @param imageUrl Image Url + * @param param Param to get from the URL. + * @param siteUrl Site URL. + * @returns Param from the URL. + */ + static getThemeImageUrlParam(imageUrl: string, param: string, siteUrl?: string): string { + if (!CoreUrl.isThemeImageUrl(imageUrl, siteUrl)) { + // Cannot be guessed. + return ''; + } + + const matches = imageUrl.match('/theme/image.php/(.*)'); + if (matches?.[1]) { + // Slash arguments found. + const slasharguments = matches[1].split('/'); + + if (slasharguments.length < 4) { + // Image not found, malformed URL. + return ''; + } + + // Join from the third element to the end. + const image = slasharguments.slice(3).join('/'); + switch (param) { + case 'theme': + return slasharguments[0]; + case 'component': + return slasharguments[1]; + case 'rev': + return slasharguments[2]; + case 'image': + // Remove possible url params. + return CoreUrl.removeUrlParts(image, [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment]); + default: + return CoreUrl.extractUrlParams(image)[param] || ''; + } + + } + + // URL arguments found. + const iconParams = CoreUrl.extractUrlParams(imageUrl); + + switch (param) { + case 'theme': + return iconParams[param] || 'standard'; + case 'component': + return iconParams[param] || 'core'; + case 'rev': + return iconParams[param] || '-1'; + case 'svg': + return iconParams[param] || '1'; + case 'image': + default: + return iconParams[param] || ''; + } + } + + /** + * Returns the URL without the desired parts. + * + * @param url URL to treat. + * @param parts Parts to remove. + * @returns URL without the parts. + */ + static removeUrlParts(url: string, parts: CoreUrlPartNames | CoreUrlPartNames[]): string { + if (!url) { + return url; + } + + if (!Array.isArray(parts)) { + parts = [parts]; + } + + parts.forEach((part) => { + switch (part) { + case CoreUrlPartNames.WWWInDomain: + // Remove www, no protocol. + url = url.replace(/^www./, ''); + // Remove www, with protocol. + url = url.replace(/\/\/www./, '//'); + break; + case CoreUrlPartNames.Protocol: + // Remove the protocol from url + url = url.replace(/^.*?:\/\//, ''); + break; + case CoreUrlPartNames.Query: + url = url.match(/^[^?]+/)?.[0] || ''; + break; + case CoreUrlPartNames.Fragment: + url = url.split('#')[0]; + break; + } + }); + + return url; + } + + /** + * Modifies a pluginfile URL to use the default pluginfile script instead of the webservice one. + * + * @param url The url to be fixed. + * @param siteUrl The URL of the site the URL belongs to. + * @returns Modified URL. + */ + static unfixPluginfileURL(url: string, siteUrl?: string): string { + if (!url) { + return ''; + } + + url = url.replace(/&/g, '&'); + + // It site URL is supplied, check if the URL belongs to the site. + if (siteUrl && url.indexOf(CoreTextUtils.addEndingSlash(siteUrl)) !== 0) { + return url; + } + + // Check tokenpluginfile first. + url = url.replace(/\/tokenpluginfile\.php\/[^/]+\//, '/pluginfile.php/'); + + // Treat webservice/pluginfile case. + url = url.replace(/\/webservice\/pluginfile\.php\//, '/pluginfile.php/'); + + // Make sure the URL doesn't contain the token. + url = url.replace(/([?&])token=[^&]*&?/, '$1'); + + return url; + } + } + +export type CoreUrlParams = {[key: string]: string}; diff --git a/src/core/singletons/window.ts b/src/core/singletons/window.ts index e3323e20b..c520b9741 100644 --- a/src/core/singletons/window.ts +++ b/src/core/singletons/window.ts @@ -18,7 +18,7 @@ import { CoreConfig } from '@services/config'; import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreConstants } from '../constants'; @@ -40,7 +40,7 @@ export class CoreWindow { * @returns Promise resolved if confirmed, rejected if rejected. */ static async confirmOpenBrowserIfNeeded(url: string): Promise { - if (!CoreUrlUtils.isHttpURL(url)) { + if (!CoreUrl.isHttpURL(url)) { // Only ask confirm for http(s), other cases usually launch external apps. return; } @@ -76,7 +76,7 @@ export class CoreWindow { * @returns Promise resolved when done. */ static async open(url: string, name?: string): Promise { - if (CoreUrlUtils.isLocalFileUrl(url)) { + if (CoreUrl.isLocalFileUrl(url)) { const filename = url.substring(url.lastIndexOf('/') + 1); if (!CoreFileHelper.isOpenableInApp({ filename })) {