MOBILE-3565 contentlinks: Add content links delegate structure

main
Pau Ferrer Ocaña 2020-11-12 10:09:32 +01:00
parent f3ae7e5e4a
commit dd43b9460b
13 changed files with 1184 additions and 5 deletions

View File

@ -0,0 +1,116 @@
// (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 { Params } from '@angular/router';
import { CoreContentLinksHandler, CoreContentLinksAction } from '../services/contentlinks.delegate';
/**
* Base handler to be registered in CoreContentLinksHandler. It is useful to minimize the amount of
* functions that handlers need to implement.
*
* It allows you to specify a "pattern" (RegExp) that will be used to check if the handler handles a URL and to get its site URL.
*/
export class CoreContentLinksHandlerBase implements CoreContentLinksHandler {
/**
* A name to identify the handler.
*/
name = 'CoreContentLinksHandlerBase';
/**
* Handler's priority. The highest priority is treated first.
*/
priority = 0;
/**
* Whether the isEnabled function should be called for all the users in a site. It should be true only if the isEnabled call
* can return different values for different users in same site.
*/
checkAllUsers = false;
/**
* Name of the feature this handler is related to.
* It will be used to check if the feature is disabled (@see CoreSite.isFeatureDisabled).
*/
featureName = '';
/**
* A pattern to use to detect if the handler handles a URL and to get its site URL. Required if "handles" and
* "getSiteUrl" functions aren't overridden.
*/
pattern?: RegExp;
/**
* Get the list of actions for a link (url).
*
* @param siteIds List of sites the URL belongs to.
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param courseId Course ID related to the URL. Optional but recommended.
* @return List of (or promise resolved with list of) actions.
*/
getActions(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
siteIds: string[],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
url: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
params: Params,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
courseId?: number,
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [];
}
/**
* Check if a URL is handled by this handler.
*
* @param url The URL to check.
* @return Whether the URL is handled by this handler
*/
handles(url: string): boolean {
return !!this.pattern && url.search(this.pattern) >= 0;
}
/**
* If the URL is handled by this handler, return the site URL.
*
* @param url The URL to check.
* @return Site URL if it is handled, undefined otherwise.
*/
getSiteUrl(url: string): string | undefined {
if (this.pattern) {
const position = url.search(this.pattern);
if (position > -1) {
return url.substr(0, position);
}
}
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param siteId The site ID.
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param courseId Course ID related to the URL. Optional but recommended.
* @return Whether the handler is enabled for the URL and site.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isEnabled(siteId: string, url: string, params: Params, courseId?: number): boolean | Promise<boolean> {
return true;
}
}

View File

@ -0,0 +1,114 @@
// (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 { CoreContentLinksAction } from '../services/contentlinks.delegate';
import { CoreContentLinksHandlerBase } from './base-handler';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
// import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { Params } from '@angular/router';
/**
* Handler to handle URLs pointing to the grade of a module.
*/
export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerBase {
/**
* Whether the module can be reviewed in the app. If true, the handler needs to implement the goToReview function.
*/
canReview = false;
/**
* If this boolean is set to true, the app will retrieve all modules with this modName with a single WS call.
* This reduces the number of WS calls, but it isn't recommended for modules that can return a lot of contents.
*/
protected useModNameToGetModule = false;
/**
* Construct the handler.
*
* @param addon Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled.
* @param modName Name of the module (assign, book, ...).
*/
constructor(
public addon: string,
public modName: string,
) {
super();
// Match the grade.php URL with an id param.
this.pattern = new RegExp('/mod/' + modName + '/grade.php.*([&?]id=\\d+)');
this.featureName = 'CoreCourseModuleDelegate_' + addon;
}
/**
* Get the list of actions for a link (url).
*
* @param siteIds Unused. List of sites the URL belongs to.
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param courseId Course ID related to the URL. Optional but recommended.
* @return List of (or promise resolved with list of) actions.
*/
getActions(
siteIds: string[],
url: string,
params: Params,
courseId?: number,
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
courseId = courseId || params.courseid || params.cid;
return [{
action: async (siteId): Promise<void> => {
// Check if userid is the site's current user.
const modal = await CoreDomUtils.instance.showModalLoading();
const site = await CoreSites.instance.getSite(siteId);
if (!params.userid || params.userid == site.getUserId()) {
// No user specified or current user. Navigate to module.
// @todo this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId, undefined,
// this.useModNameToGetModule ? this.modName : undefined, undefined, navCtrl);
} else if (this.canReview) {
// Use the goToReview function.
this.goToReview(url, params, courseId!, siteId);
} else {
// Not current user and cannot review it in the app, open it in browser.
site.openInBrowserWithAutoLogin(url);
}
modal.dismiss();
},
}];
}
/**
* Go to the page to review.
*
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param courseId Course ID related to the URL.
* @param siteId Site to use.
* @return Promise resolved when done.
*/
protected async goToReview(
url: string, // eslint-disable-line @typescript-eslint/no-unused-vars
params: Params, // eslint-disable-line @typescript-eslint/no-unused-vars
courseId: number, // eslint-disable-line @typescript-eslint/no-unused-vars
siteId: string, // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<void> {
// This function should be overridden.
return;
}
}

View File

@ -0,0 +1,106 @@
// (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 { CoreContentLinksHandlerBase } from './base-handler';
import { Params } from '@angular/router';
import { CoreContentLinksAction } from '../services/contentlinks.delegate';
/**
* Handler to handle URLs pointing to the index of a module.
*/
export class CoreContentLinksModuleIndexHandler extends CoreContentLinksHandlerBase {
/**
* If this boolean is set to true, the app will retrieve all modules with this modName with a single WS call.
* This reduces the number of WS calls, but it isn't recommended for modules that can return a lot of contents.
*/
protected useModNameToGetModule = false;
/**
* Construct the handler.
*
* @param addon Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled.
* @param modName Name of the module (assign, book, ...).
* @param instanceIdParam Param name for instance ID gathering. Only if set.
*/
constructor(
public addon: string,
public modName: string,
protected instanceIdParam?: string,
) {
super();
const pattern = instanceIdParam ?
'/mod/' + modName + '/view.php.*([&?](' + instanceIdParam + '|id)=\\d+)' :
'/mod/' + modName + '/view.php.*([&?]id=\\d+)';
// Match the view.php URL with an id param.
this.pattern = new RegExp(pattern);
this.featureName = 'CoreCourseModuleDelegate_' + addon;
}
/**
* Get the mod params necessary to open an activity.
*
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param courseId Course ID related to the URL. Optional but recommended.
* @return List of params to pass to navigateToModule / navigateToModuleByInstance.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getPageParams(url: string, params: Params, courseId?: number): Params {
return [];
}
/**
* Get the list of actions for a link (url).
*
* @param siteIds List of sites the URL belongs to.
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param courseId Course ID related to the URL. Optional but recommended.
* @return List of (or promise resolved with list of) actions.
*/
getActions(
siteIds: string[], // eslint-disable-line @typescript-eslint/no-unused-vars
url: string, // eslint-disable-line @typescript-eslint/no-unused-vars
params: Params, // eslint-disable-line @typescript-eslint/no-unused-vars
courseId?: number, // eslint-disable-line @typescript-eslint/no-unused-vars
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [];
/*
courseId = courseId || params.courseid || params.cid;
const pageParams = this.getPageParams(url, params, courseId);
if (this.instanceIdParam && typeof params[this.instanceIdParam] != 'undefined') {
const instanceId = parseInt(params[this.instanceIdParam], 10);
return [{
action: (siteId): void => {
this.courseHelper.navigateToModuleByInstance(instanceId, this.modName, siteId, courseId, undefined,
this.useModNameToGetModule, pageParams);
},
}];
}
return [{
action: (siteId): void => {
this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId, undefined,
this.useModNameToGetModule ? this.modName : undefined, pageParams);
},
}];
*/
}
}

View File

@ -0,0 +1,73 @@
// (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 { CoreContentLinksHelper } from '../services/contentlinks.helper';
import { CoreContentLinksHandlerBase } from './base-handler';
import { Translate } from '@/app/singletons/core.singletons';
import { Params } from '@angular/router';
import { CoreContentLinksAction } from '../services/contentlinks.delegate';
/**
* Handler to handle URLs pointing to a list of a certain type of modules.
*/
export class CoreContentLinksModuleListHandler extends CoreContentLinksHandlerBase {
/**
* The title to use in the new page. If not defined, the app will try to calculate it.
*/
protected title = '';
/**
* Construct the handler.
*
* @param linkHelper The CoreContentLinksHelperProvider instance.
* @param translate The TranslateService instance.
* @param addon Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled.
* @param modName Name of the module (assign, book, ...).
*/
constructor(
public addon: string,
public modName: string,
) {
super();
// Match the view.php URL with an id param.
this.pattern = new RegExp('/mod/' + modName + '/index.php.*([&?]id=\\d+)');
this.featureName = 'CoreCourseModuleDelegate_' + addon;
}
/**
* Get the list of actions for a link (url).
*
* @param siteIds List of sites the URL belongs to.
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @return List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: (siteId): void => {
const stateParams = {
courseId: params.id,
modName: this.modName,
title: this.title || Translate.instance.instant('addon.mod_' + this.modName + '.modulenameplural'),
};
CoreContentLinksHelper.instance.goInSite('CoreCourseListModTypePage @todo', stateParams, siteId);
},
}];
}
}

View File

@ -0,0 +1,8 @@
{
"chooseaccount": "Choose account",
"chooseaccounttoopenlink": "Choose an account to open the link with.",
"confirmurlothersite": "This link belongs to another site. Do you want to open it?",
"errornoactions": "Couldn't find an action to perform with this link.",
"errornosites": "Couldn't find any site to handle this link.",
"errorredirectothersite": "The redirect URL cannot point to a different site."
}

View File

@ -0,0 +1,31 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'core.contentlinks.chooseaccount' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-list>
<ion-item class="ion-text-wrap">
<p class="item-heading">{{ 'core.contentlinks.chooseaccounttoopenlink' | translate }}</p>
<p>{{ url }}</p>
</ion-item>
<ion-item *ngFor="let site of sites" (click)="siteClicked(site.id)" detail="false">
<ion-avatar slot="start">
<img [src]="site.avatar" core-external-content [siteId]="site.id"
alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}" role="presentation"
onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<h2>{{site.fullName}}</h2>
<p><core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text></p>
<p>{{site.siteUrl}}</p>
</ion-item>
<ion-item>
<ion-button expand="block" (click)="cancel()">{{ 'core.login.cancel' | translate }}</ion-button>
</ion-item>
</ion-list>
</core-loading>
</ion-content>

View File

@ -0,0 +1,47 @@
// (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 { NgModule } from '@angular/core';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreContentLinksChooseSitePage } from './choose-site.page';
const routes: Routes = [
{
path: '',
component: CoreContentLinksChooseSitePage,
},
];
@NgModule({
declarations: [
CoreContentLinksChooseSitePage,
],
imports: [
RouterModule.forChild(routes),
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
],
})
export class CoreContentLinksChooseSitePageModule {}

View File

@ -0,0 +1,122 @@
// (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 { Component, OnInit } from '@angular/core';
import { NavController } from '@ionic/angular';
import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { Translate } from '@singletons/core.singletons';
import { CoreLoginHelper } from '@core/login/services/helper';
import { CoreContentLinksAction } from '../../services/contentlinks.delegate';
import { CoreContentLinksHelper } from '../../services/contentlinks.helper';
import { ActivatedRoute } from '@angular/router';
import { CoreError } from '@classes/errors/error';
/**
* Page to display the list of sites to choose one to perform a content link action.
*
* @todo Include routing and testing.
*/
@Component({
selector: 'page-core-content-links-choose-site',
templateUrl: 'choose-site.html',
})
export class CoreContentLinksChooseSitePage implements OnInit {
url: string;
sites: CoreSiteBasicInfo[] = [];
loaded = false;
protected action?: CoreContentLinksAction;
protected isRootURL = false;
constructor(
route: ActivatedRoute,
protected navCtrl: NavController,
) {
this.url = route.snapshot.queryParamMap.get('url')!;
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
if (!this.url) {
return this.leaveView();
}
let siteIds: string[] | undefined = [];
try {
// Check if it's the root URL.
const data = await CoreSites.instance.isStoredRootURL(this.url);
if (data.site) {
// It's the root URL.
this.isRootURL = true;
siteIds = data.siteIds;
} else if (data.siteIds.length) {
// Not root URL, but the URL belongs to at least 1 site. Check if there is any action to treat the link.
this.action = await CoreContentLinksHelper.instance.getFirstValidActionFor(this.url);
if (!this.action) {
throw new CoreError(Translate.instance.instant('core.contentlinks.errornoactions'));
}
siteIds = this.action.sites;
} else {
// No sites to treat the URL.
throw new CoreError(Translate.instance.instant('core.contentlinks.errornosites'));
}
// Get the sites that can perform the action.
this.sites = await CoreSites.instance.getSites(siteIds);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.contentlinks.errornosites', true);
this.leaveView();
}
this.loaded = true;
}
/**
* Cancel.
*/
cancel(): void {
this.leaveView();
}
/**
* Perform the action on a certain site.
*
* @param siteId Site ID.
*/
siteClicked(siteId: string): void {
if (this.isRootURL) {
CoreLoginHelper.instance.redirect('', {}, siteId);
} else if (this.action) {
this.action.action(siteId);
}
}
/**
* Cancel and leave the view.
*/
protected async leaveView(): Promise<void> {
try {
await CoreSites.instance.logout();
} finally {
await this.navCtrl.navigateRoot('/login/sites');
}
}
}

View File

@ -0,0 +1,309 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreLogger } from '@singletons/logger';
import { CoreSites } from '@services/sites';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils';
import { Params } from '@angular/router';
/**
* Interface that all handlers must implement.
*/
export interface CoreContentLinksHandler {
/**
* A name to identify the handler.
*/
name: string;
/**
* Handler's priority. The highest priority is treated first.
*/
priority?: number;
/**
* Whether the isEnabled function should be called for all the users in a site. It should be true only if the isEnabled call
* can return different values for different users in same site.
*/
checkAllUsers?: boolean;
/**
* Name of the feature this handler is related to.
* It will be used to check if the feature is disabled (@see CoreSite.isFeatureDisabled).
*/
featureName?: string;
/**
* Get the list of actions for a link (url).
*
* @param siteIds List of sites the URL belongs to.
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param courseId Course ID related to the URL. Optional but recommended.
* @param data Extra data to handle the URL.
* @return List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: Params, courseId?: number, data?: unknown):
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]>;
/**
* Check if a URL is handled by this handler.
*
* @param url The URL to check.
* @return Whether the URL is handled by this handler
*/
handles(url: string): boolean;
/**
* If the URL is handled by this handler, return the site URL.
*
* @param url The URL to check.
* @return Site URL if it is handled, undefined otherwise.
*/
getSiteUrl(url: string): string | undefined;
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param siteId The site ID.
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param courseId Course ID related to the URL. Optional but recommended.
* @return Whether the handler is enabled for the URL and site.
*/
isEnabled?(siteId: string, url: string, params: Params, courseId?: number): boolean | Promise<boolean>;
}
/**
* Action to perform when a link is clicked.
*/
export interface CoreContentLinksAction {
/**
* A message to identify the action. Default: 'core.view'.
*/
message?: string;
/**
* Name of the icon of the action. Default: 'fas-eye'.
*/
icon?: string;
/**
* IDs of the sites that support the action.
*/
sites?: string[];
/**
* Action to perform when the link is clicked.
*
* @param siteId The site ID.
*/
action(siteId: string): void;
}
/**
* Actions and priority for a handler and URL.
*/
export interface CoreContentLinksHandlerActions {
/**
* Handler's priority.
*/
priority: number;
/**
* List of actions.
*/
actions: CoreContentLinksAction[];
}
/**
* Delegate to register handlers to handle links.
*/
@Injectable({
providedIn: 'root',
})
export class CoreContentLinksDelegate {
protected logger: CoreLogger;
protected handlers: { [s: string]: CoreContentLinksHandler } = {}; // All registered handlers.
constructor() {
this.logger = CoreLogger.getInstance('CoreContentLinksDelegate');
}
/**
* Get the list of possible actions to do for a URL.
*
* @param url URL to handle.
* @param courseId Course ID related to the URL. Optional but recommended.
* @param username Username to use to filter sites.
* @param data Extra data to handle the URL.
* @return Promise resolved with the actions.
*/
async getActionsFor(url: string, courseId?: number, username?: string, data?: unknown): Promise<CoreContentLinksAction[]> {
if (!url) {
return [];
}
// Get the list of sites the URL belongs to.
const siteIds = await CoreSites.instance.getSiteIdsFromUrl(url, true, username);
const linkActions: CoreContentLinksHandlerActions[] = [];
const promises: Promise<void>[] = [];
const params = CoreUrlUtils.instance.extractUrlParams(url);
for (const name in this.handlers) {
const handler = this.handlers[name];
const checkAll = handler.checkAllUsers;
const isEnabledFn = this.isHandlerEnabled.bind(this, handler, url, params, courseId);
if (!handler.handles(url)) {
// Invalid handler or it doesn't handle the URL. Stop.
continue;
}
// Filter the site IDs using the isEnabled function.
promises.push(CoreUtils.instance.filterEnabledSites(siteIds, isEnabledFn, checkAll).then(async (siteIds) => {
if (!siteIds.length) {
// No sites supported, no actions.
return;
}
const actions = await handler.getActions(siteIds, url, params, courseId, data);
if (actions && actions.length) {
// Set default values if any value isn't supplied.
actions.forEach((action) => {
action.message = action.message || 'core.view';
action.icon = action.icon || 'fas-eye';
action.sites = action.sites || siteIds;
});
// Add them to the list.
linkActions.push({
priority: handler.priority || 0,
actions: actions,
});
}
return;
}));
}
try {
await CoreUtils.instance.allPromises(promises);
} catch {
// Ignore errors.
}
// Sort link actions by priority.
return this.sortActionsByPriority(linkActions);
}
/**
* Get the site URL if the URL is supported by any handler.
*
* @param url URL to handle.
* @return Site URL if the URL is supported by any handler, undefined otherwise.
*/
getSiteUrl(url: string): string | void {
if (!url) {
return;
}
// Check if any handler supports this URL.
for (const name in this.handlers) {
const handler = this.handlers[name];
const siteUrl = handler.getSiteUrl(url);
if (siteUrl) {
return siteUrl;
}
}
}
/**
* Check if a handler is enabled for a certain site and URL.
*
* @param handler Handler to check.
* @param url The URL to check.
* @param params The params of the URL
* @param courseId Course ID the URL belongs to (can be undefined).
* @param siteId The site ID to check.
* @return Promise resolved with boolean: whether the handler is enabled.
*/
protected async isHandlerEnabled(
handler: CoreContentLinksHandler,
url: string,
params: Params,
courseId: number,
siteId: string,
): Promise<boolean> {
let disabled = false;
if (handler.featureName) {
// Check if the feature is disabled.
disabled = await CoreSites.instance.isFeatureDisabled(handler.featureName, siteId);
}
if (disabled) {
return false;
}
if (!handler.isEnabled) {
// Handler doesn't implement isEnabled, assume it's enabled.
return true;
}
return handler.isEnabled(siteId, url, params, courseId);
}
/**
* Register a handler.
*
* @param handler The handler to register.
* @return True if registered successfully, false otherwise.
*/
registerHandler(handler: CoreContentLinksHandler): boolean {
if (typeof this.handlers[handler.name] !== 'undefined') {
this.logger.log(`Addon '${handler.name}' already registered`);
return false;
}
this.logger.log(`Registered addon '${handler.name}'`);
this.handlers[handler.name] = handler;
return true;
}
/**
* Sort actions by priority.
*
* @param actions Actions to sort.
* @return Sorted actions.
*/
protected sortActionsByPriority(actions: CoreContentLinksHandlerActions[]): CoreContentLinksAction[] {
let sorted: CoreContentLinksAction[] = [];
// Sort by priority.
actions = actions.sort((a, b) => (a.priority || 0) <= (b.priority || 0) ? 1 : -1);
// Fill result array.
actions.forEach((entry) => {
sorted = sorted.concat(entry.actions);
});
return sorted;
}
}

View File

@ -0,0 +1,246 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { NavController } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreLoginHelper } from '@core/login/services/helper';
import { CoreContentLinksDelegate, CoreContentLinksAction } from './contentlinks.delegate';
import { CoreSite } from '@classes/site';
import { CoreMainMenu } from '@core/mainmenu/services/mainmenu';
import { makeSingleton, NgZone, Translate } from '@singletons/core.singletons';
import { Params } from '@angular/router';
/**
* Service that provides some features regarding content links.
*/
@Injectable({
providedIn: 'root',
})
export class CoreContentLinksHelperProvider {
constructor(
protected contentLinksDelegate: CoreContentLinksDelegate,
protected navCtrl: NavController,
) { }
/**
* Check whether a link can be handled by the app.
*
* @param url URL to handle.
* @param courseId Unused param: Course ID related to the URL.
* @param username Username to use to filter sites.
* @param checkRoot Whether to check if the URL is the root URL of a site.
* @return Promise resolved with a boolean: whether the URL can be handled.
*/
async canHandleLink(url: string, courseId?: number, username?: string, checkRoot?: boolean): Promise<boolean> {
try {
if (checkRoot) {
const data = await CoreSites.instance.isStoredRootURL(url, username);
if (data.site) {
// URL is the root of the site, can handle it.
return true;
}
}
const action = await this.getFirstValidActionFor(url, undefined, username);
return !!action;
} catch {
return false;
}
}
/**
* Get the first valid action in the list of possible actions to do for a URL.
*
* @param url URL to handle.
* @param courseId Course ID related to the URL. Optional but recommended.
* @param username Username to use to filter sites.
* @param data Extra data to handle the URL.
* @return Promise resolved with the first valid action. Returns undefined if no valid action found..
*/
async getFirstValidActionFor(
url: string,
courseId?: number,
username?: string,
data?: unknown,
): Promise<CoreContentLinksAction | undefined> {
const actions = await this.contentLinksDelegate.getActionsFor(url, courseId, username, data);
if (!actions) {
return;
}
return actions.find((action) => action && action.sites && action.sites.length);
}
/**
* Goes to a certain page in a certain site. If the site is current site it will perform a regular navigation,
* otherwise it will 'redirect' to the other site.
*
* @param pageName Name of the page to go.
* @param pageParams Params to send to the page.
* @param siteId Site ID. If not defined, current site.
* @param checkMenu If true, check if the root page of a main menu tab. Only the page name will be checked.
* @return Promise resolved when done.
*/
goInSite(
pageName: string,
pageParams: Params,
siteId?: string,
checkMenu?: boolean,
): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const deferred = CoreUtils.instance.promiseDefer<void>();
// Execute the code in the Angular zone, so change detection doesn't stop working.
NgZone.instance.run(async () => {
try {
if (siteId == CoreSites.instance.getCurrentSiteId()) {
if (checkMenu) {
let isInMenu = false;
// Check if the page is in the main menu.
try {
isInMenu = await CoreMainMenu.instance.isCurrentMainMenuHandler(pageName);
} catch {
isInMenu = false;
}
if (isInMenu) {
// Just select the tab. @todo test.
CoreLoginHelper.instance.loadPageInMainMenu(pageName, pageParams);
} else {
await this.navCtrl.navigateForward(pageName, { queryParams: pageParams });
}
} else {
await this.navCtrl.navigateForward(pageName, { queryParams: pageParams });
}
} else {
await CoreLoginHelper.instance.redirect(pageName, pageParams, siteId);
}
deferred.resolve();
} catch (error) {
deferred.reject(error);
}
});
return deferred.promise;
}
/**
* Go to the page to choose a site.
*
* @param url URL to treat.
* @todo set correct root.
*/
async goToChooseSite(url: string): Promise<void> {
await this.navCtrl.navigateRoot('CoreContentLinksChooseSitePage @todo', { queryParams: { url } });
}
/**
* Handle a link.
*
* @param url URL to handle.
* @param username Username related with the URL. E.g. in 'http://myuser@m.com', url would be 'http://m.com' and
* the username 'myuser'. Don't use it if you don't want to filter by username.
* @param checkRoot Whether to check if the URL is the root URL of a site.
* @param openBrowserRoot Whether to open in browser if it's root URL and it belongs to current site.
* @return Promise resolved with a boolean: true if URL was treated, false otherwise.
*/
async handleLink(
url: string,
username?: string,
checkRoot?: boolean,
openBrowserRoot?: boolean,
): Promise<boolean> {
try {
if (checkRoot) {
const data = await CoreSites.instance.isStoredRootURL(url, username);
if (data.site) {
// URL is the root of the site.
this.handleRootURL(data.site, openBrowserRoot);
return true;
}
}
// Check if the link should be treated by some component/addon.
const action = await this.getFirstValidActionFor(url, undefined, username);
if (!action) {
return false;
}
if (!CoreSites.instance.isLoggedIn()) {
// No current site. Perform the action if only 1 site found, choose the site otherwise.
if (action.sites?.length == 1) {
action.action(action.sites[0]);
} else {
this.goToChooseSite(url);
}
} else if (action.sites?.length == 1 && action.sites[0] == CoreSites.instance.getCurrentSiteId()) {
// Current site.
action.action(action.sites[0]);
} else {
try {
// Not current site or more than one site. Ask for confirmation.
await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.contentlinks.confirmurlothersite'));
if (action.sites?.length == 1) {
action.action(action.sites[0]);
} else {
this.goToChooseSite(url);
}
} catch {
// User canceled.
}
}
return true;
} catch {
// Ignore errors.
}
return false;
}
/**
* Handle a root URL of a site.
*
* @param site Site to handle.
* @param openBrowserRoot Whether to open in browser if it's root URL and it belongs to current site.
* @param checkToken Whether to check that token is the same to verify it's current site. If false or not defined,
* only the URL will be checked.
* @return Promise resolved when done.
*/
async handleRootURL(site: CoreSite, openBrowserRoot?: boolean, checkToken?: boolean): Promise<void> {
const currentSite = CoreSites.instance.getCurrentSite();
if (currentSite && currentSite.getURL() == site.getURL() && (!checkToken || currentSite.getToken() == site.getToken())) {
// Already logged in.
if (openBrowserRoot) {
return site.openInBrowserWithAutoLogin(site.getURL());
}
} else {
// Login in the site.
return CoreLoginHelper.instance.redirect('', {}, site.getId());
}
}
}
export class CoreContentLinksHelper extends makeSingleton(CoreContentLinksHelperProvider) {}

View File

@ -26,6 +26,7 @@ import { CoreConfig } from '@services/config';
import { CoreDomUtils } from '@services/utils/dom';
// import { CoreCourseProvider } from '@core/course/providers/course';
import { makeSingleton, Translate } from '@singletons/core.singletons';
import { CoreError } from '@classes/errors/error';
/**
* Object with space usage and cache entries that can be erased.
@ -281,12 +282,12 @@ export class CoreSettingsHelperProvider {
if (site.isLoggedOut()) {
// Cannot sync logged out sites.
throw Translate.instance.instant('core.settings.cannotsyncloggedout');
throw new CoreError(Translate.instance.instant('core.settings.cannotsyncloggedout'));
} else if (hasSyncHandlers && !CoreApp.instance.isOnline()) {
// We need connection to execute sync.
throw Translate.instance.instant('core.settings.cannotsyncoffline');
throw new CoreError(Translate.instance.instant('core.settings.cannotsyncoffline'));
} else if (hasSyncHandlers && syncOnlyOnWifi && CoreApp.instance.isNetworkAccessLimited()) {
throw Translate.instance.instant('core.settings.cannotsyncwithoutwifi');
throw new CoreError(Translate.instance.instant('core.settings.cannotsyncwithoutwifi'));
}
const syncPromise = Promise.all([
@ -329,7 +330,7 @@ export class CoreSettingsHelperProvider {
// Local mobile was added. Throw invalid session to force reconnect and create a new token.
CoreEvents.trigger(CoreEvents.SESSION_EXPIRED, {}, site.getId());
throw Translate.instance.instant('core.lostconnection');
throw new CoreError(Translate.instance.instant('core.lostconnection'));
}
/**

View File

@ -231,7 +231,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
if (!site.canDownloadFiles() && CoreUrlUtils.instance.isPluginFileUrl(url)) {
this.element.parentElement?.removeChild(this.element); // Remove element since it'll be broken.
throw 'Site doesn\'t allow downloading files.';
throw new CoreError('Site doesn\'t allow downloading files.');
}
// Download images, tracks and posters if size is unknown.

View File

@ -353,6 +353,12 @@
"core.considereddigitalminor": "You are too young to create an account on this site.",
"core.content": "Content",
"core.contenteditingsynced": "The content you are editing has been synced.",
"core.contentlinks.chooseaccount": "Choose account",
"core.contentlinks.chooseaccounttoopenlink": "Choose an account to open the link with.",
"core.contentlinks.confirmurlothersite": "This link belongs to another site. Do you want to open it?",
"core.contentlinks.errornoactions": "Couldn't find an action to perform with this link.",
"core.contentlinks.errornosites": "Couldn't find any site to handle this link.",
"core.contentlinks.errorredirectothersite": "The redirect URL cannot point to a different site.",
"core.continue": "Continue",
"core.copiedtoclipboard": "Text copied to clipboard",
"core.copytoclipboard": "Copy to clipboard",