MOBILE-3565 core: Migrate some core classes

main
Dani Palou 2020-10-07 10:52:51 +02:00
parent 1e979b57bb
commit 811bb39781
9 changed files with 3846 additions and 4 deletions

View File

@ -0,0 +1,352 @@
// (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 { CoreSites } from '@services/sites';
import { CoreEvents, CoreEventsProvider } from '@services/events';
import { CoreSite } from '@classes/site';
import { CoreLogger } from '@singletons/logger';
/**
* Superclass to help creating delegates
*/
export class CoreDelegate {
/**
* Logger instance.
*/
protected logger: CoreLogger;
/**
* List of registered handlers.
*/
protected handlers: { [s: string]: CoreDelegateHandler } = {};
/**
* List of registered handlers enabled for the current site.
*/
protected enabledHandlers: { [s: string]: CoreDelegateHandler } = {};
/**
* Default handler
*/
protected defaultHandler: CoreDelegateHandler;
/**
* Time when last updateHandler functions started.
*/
protected lastUpdateHandlersStart: number;
/**
* Feature prefix to check is feature is enabled or disabled in site.
* This check is only made if not false. Override on the subclass or override isFeatureDisabled function.
*/
protected featurePrefix: string;
/**
* Name of the property to be used to index the handlers. By default, the handler's name will be used.
* If your delegate uses a Moodle component name to identify the handlers, please override this property.
* E.g. CoreCourseModuleDelegate uses 'modName' to index the handlers.
*/
protected handlerNameProperty = 'name';
/**
* Set of promises to update a handler, to prevent doing the same operation twice.
*/
protected updatePromises: {[siteId: string]: {[name: string]: Promise<any>}} = {};
/**
* Whether handlers have been initialized.
*/
protected handlersInitialized = false;
/**
* Promise to wait for handlers to be initialized.
*/
protected handlersInitPromise: Promise<any>;
/**
* Function to resolve the handlers init promise.
*/
protected handlersInitResolve: (value?: any) => void;
/**
* Constructor of the Delegate.
*
* @param delegateName Delegate name used for logging purposes.
* @param listenSiteEvents Whether to update the handler when a site event occurs (login, site updated, ...).
*/
constructor(delegateName: string, listenSiteEvents?: boolean) {
this.logger = CoreLogger.getInstance(delegateName);
this.handlersInitPromise = new Promise((resolve): void => {
this.handlersInitResolve = resolve;
});
if (listenSiteEvents) {
// Update handlers on this cases.
CoreEvents.instance.on(CoreEventsProvider.LOGIN, this.updateHandlers.bind(this));
CoreEvents.instance.on(CoreEventsProvider.SITE_UPDATED, this.updateHandlers.bind(this));
CoreEvents.instance.on(CoreEventsProvider.SITE_PLUGINS_LOADED, this.updateHandlers.bind(this));
}
}
/**
* Execute a certain function in a enabled handler.
* If the handler isn't found or function isn't defined, call the same function in the default handler.
*
* @param handlerName The handler name.
* @param fnName Name of the function to execute.
* @param params Parameters to pass to the function.
* @return Function returned value or default value.
*/
protected executeFunctionOnEnabled(handlerName: string, fnName: string, params?: any[]): any {
return this.execute(this.enabledHandlers[handlerName], fnName, params);
}
/**
* Execute a certain function in a handler.
* If the handler isn't found or function isn't defined, call the same function in the default handler.
*
* @param handlerName The handler name.
* @param fnName Name of the function to execute.
* @param params Parameters to pass to the function.
* @return Function returned value or default value.
*/
protected executeFunction(handlerName: string, fnName: string, params?: any[]): any {
return this.execute(this.handlers[handlerName], fnName, params);
}
/**
* Execute a certain function in a handler.
* If the handler isn't found or function isn't defined, call the same function in the default handler.
*
* @param handler The handler.
* @param fnName Name of the function to execute.
* @param params Parameters to pass to the function.
* @return Function returned value or default value.
*/
private execute(handler: any, fnName: string, params?: any[]): any {
if (handler && handler[fnName]) {
return handler[fnName].apply(handler, params);
} else if (this.defaultHandler && this.defaultHandler[fnName]) {
return this.defaultHandler[fnName].apply(this.defaultHandler, params);
}
}
/**
* Get a handler.
*
* @param handlerName The handler name.
* @param enabled Only enabled, or any.
* @return Handler.
*/
protected getHandler(handlerName: string, enabled: boolean = false): CoreDelegateHandler {
return enabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName];
}
/**
* Gets the handler full name for a given name. This is useful when the handlerNameProperty is different than "name".
* E.g. blocks are indexed by blockName. If you call this function passing the blockName it will return the name.
*
* @param name Name used to indentify the handler.
* @return Full name of corresponding handler.
*/
getHandlerName(name: string): string {
const handler = this.getHandler(name, true);
if (!handler) {
return '';
}
return handler.name;
}
/**
* Check if function exists on a handler.
*
* @param handlerName The handler name.
* @param fnName Name of the function to execute.
* @param onlyEnabled If check only enabled handlers or all.
* @return Function returned value or default value.
*/
protected hasFunction(handlerName: string, fnName: string, onlyEnabled: boolean = true): any {
const handler = onlyEnabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName];
return handler && handler[fnName];
}
/**
* Check if a handler name has a registered handler (not necessarily enabled).
*
* @param name The handler name.
* @param enabled Only enabled, or any.
* @return If the handler is registered or not.
*/
hasHandler(name: string, enabled: boolean = false): boolean {
return enabled ? typeof this.enabledHandlers[name] !== 'undefined' : typeof this.handlers[name] !== 'undefined';
}
/**
* Check if a time belongs to the last update handlers call.
* This is to handle the cases where updateHandlers don't finish in the same order as they're called.
*
* @param time Time to check.
* @return Whether it's the last call.
*/
isLastUpdateCall(time: number): boolean {
if (!this.lastUpdateHandlersStart) {
return true;
}
return time == this.lastUpdateHandlersStart;
}
/**
* Register a handler.
*
* @param handler The handler delegate object to register.
* @return True when registered, false if already registered.
*/
registerHandler(handler: CoreDelegateHandler): boolean {
const key = handler[this.handlerNameProperty] || handler.name;
if (typeof this.handlers[key] !== 'undefined') {
this.logger.log(`Handler '${handler[this.handlerNameProperty]}' already registered`);
return false;
}
this.logger.log(`Registered handler '${handler[this.handlerNameProperty]}'`);
this.handlers[key] = handler;
return true;
}
/**
* Update the handler for the current site.
*
* @param handler The handler to check.
* @param time Time this update process started.
* @return Resolved when done.
*/
protected updateHandler(handler: CoreDelegateHandler, time: number): Promise<void> {
const siteId = CoreSites.instance.getCurrentSiteId();
const currentSite = CoreSites.instance.getCurrentSite();
let promise;
if (this.updatePromises[siteId] && this.updatePromises[siteId][handler.name]) {
// There's already an update ongoing for this handler, return the promise.
return this.updatePromises[siteId][handler.name];
} else if (!this.updatePromises[siteId]) {
this.updatePromises[siteId] = {};
}
if (!CoreSites.instance.isLoggedIn()) {
promise = Promise.reject(null);
} else if (this.isFeatureDisabled(handler, currentSite)) {
promise = Promise.resolve(false);
} else {
promise = Promise.resolve(handler.isEnabled());
}
// Checks if the handler is enabled.
this.updatePromises[siteId][handler.name] = promise.catch(() => {
return false;
}).then((enabled: boolean) => {
// Check that site hasn't changed since the check started.
if (CoreSites.instance.getCurrentSiteId() === siteId) {
const key = handler[this.handlerNameProperty] || handler.name;
if (enabled) {
this.enabledHandlers[key] = handler;
} else {
delete this.enabledHandlers[key];
}
}
}).finally(() => {
// Update finished, delete the promise.
delete this.updatePromises[siteId][handler.name];
});
return this.updatePromises[siteId][handler.name];
}
/**
* Check if feature is enabled or disabled in the site, depending on the feature prefix and the handler name.
*
* @param handler Handler to check.
* @param site Site to check.
* @return Whether is enabled or disabled in site.
*/
protected isFeatureDisabled(handler: CoreDelegateHandler, site: CoreSite): boolean {
return typeof this.featurePrefix != 'undefined' && site.isFeatureDisabled(this.featurePrefix + handler.name);
}
/**
* Update the handlers for the current site.
*
* @return Resolved when done.
*/
protected updateHandlers(): Promise<void> {
const promises = [],
now = Date.now();
this.logger.debug('Updating handlers for current site.');
this.lastUpdateHandlersStart = now;
// Loop over all the handlers.
for (const name in this.handlers) {
promises.push(this.updateHandler(this.handlers[name], now));
}
return Promise.all(promises).then(() => {
return true;
}, () => {
// Never reject.
return true;
}).then(() => {
// Verify that this call is the last one that was started.
if (this.isLastUpdateCall(now)) {
this.handlersInitialized = true;
this.handlersInitResolve();
this.updateData();
}
});
}
/**
* Update handlers Data.
* Override this function to update handlers data.
*/
updateData(): any {
// To be overridden.
}
}
export interface CoreDelegateHandler {
/**
* Name of the handler, or name and sub context (AddonMessages, AddonMessages:blockContact, ...).
* This name will be used to check if the feature is disabled.
*/
name: string;
/**
* Whether or not the handler is enabled on a site level.
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean>;
}

View File

@ -0,0 +1,33 @@
// (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.
/**
* Base Error class.
*
* The native Error class cannot be extended in Typescript without restoring the prototype chain, extend this
* class instead.
*
* @see https://stackoverflow.com/questions/41102060/typescript-extending-error-class
*/
export class CoreError extends Error {
constructor(message?: string) {
super(message);
// Fix prototype chain: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
this.name = new.target.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}

View File

@ -0,0 +1,77 @@
// (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 { HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
/**
* Interceptor for Http calls. Adds the header 'Content-Type'='application/x-www-form-urlencoded'
* and serializes the parameters if needed.
*/
@Injectable()
export class CoreInterceptor implements HttpInterceptor {
/**
* Serialize an object to be used in a request.
*
* @param obj Object to serialize.
* @param addNull Add null values to the serialized as empty parameters.
* @return Serialization of the object.
*/
static serialize(obj: any, addNull?: boolean): string {
let query = '';
let fullSubName;
let subValue;
let innerObj;
for (const name in obj) {
const value = obj[name];
if (value instanceof Array) {
for (let i = 0; i < value.length; ++i) {
subValue = value[i];
fullSubName = name + '[' + i + ']';
innerObj = {};
innerObj[fullSubName] = subValue;
query += this.serialize(innerObj) + '&';
}
} else if (value instanceof Object) {
for (const subName in value) {
subValue = value[subName];
fullSubName = name + '[' + subName + ']';
innerObj = {};
innerObj[fullSubName] = subValue;
query += this.serialize(innerObj) + '&';
}
} else if (addNull || (typeof value != 'undefined' && value !== null)) {
query += encodeURIComponent(name) + '=' + encodeURIComponent(value) + '&';
}
}
return query.length ? query.substr(0, query.length - 1) : query;
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
// Add the header and serialize the body if needed.
const newReq = req.clone({
headers: req.headers.set('Content-Type', 'application/x-www-form-urlencoded'),
body: typeof req.body == 'object' && String(req.body) != '[object File]' ?
CoreInterceptor.serialize(req.body) : req.body
});
// Pass on the cloned request instead of the original request.
return next.handle(newReq);
}
}

View File

@ -0,0 +1,98 @@
// (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 { HttpResponse as AngularHttpResponse, HttpHeaders } from '@angular/common/http';
import { HTTPResponse as NativeHttpResponse } from '@ionic-native/http';
const HTTP_STATUS_MESSAGES = {
100: 'Continue',
101: 'Switching Protocol',
102: 'Processing',
103: 'Early Hints',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status',
208: 'Already Reported',
226: 'IM Used',
300: 'Multiple Choice',
301: 'Moved Permanently',
302: 'Found',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
306: 'unused',
307: 'Temporary Redirect',
308: 'Permanent Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Payload Too Large',
414: 'URI Too Long',
415: 'Unsupported Media Type',
416: 'Range Not Satisfiable',
417: 'Expectation Failed',
418: 'I\'m a teapot',
421: 'Misdirected Request',
422: 'Unprocessable Entity',
423: 'Locked',
424: 'Failed Dependency',
425: 'Too Early',
426: 'Upgrade Required',
428: 'Precondition Required',
429: 'Too Many Requests',
431: 'Request Header Fields Too Large',
451: 'Unavailable For Legal Reasons',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
505: 'HTTP Version Not Supported',
506: 'Variant Also Negotiates',
507: 'Insufficient Storage',
508: 'Loop Detected',
510: 'Not Extended',
511: 'Network Authentication Required',
};
/**
* Class that adapts a Cordova plugin http response to an Angular http response.
*/
export class CoreNativeToAngularHttpResponse<T> extends AngularHttpResponse<T> {
constructor(protected nativeResponse: NativeHttpResponse) {
super({
body: nativeResponse.data,
headers: new HttpHeaders(nativeResponse.headers),
status: nativeResponse.status,
statusText: HTTP_STATUS_MESSAGES[nativeResponse.status] || '',
url: nativeResponse.url || ''
});
}
}

View File

@ -0,0 +1,143 @@
// (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 { CoreUtils, PromiseDefer } from '@services/utils/utils';
/**
* Function to add to the queue.
*/
export type CoreQueueRunnerFunction<T> = (...args: any[]) => T | Promise<T>;
/**
* Queue item.
*/
export type CoreQueueRunnerItem<T = any> = {
/**
* Item ID.
*/
id: string;
/**
* Function to execute.
*/
fn: CoreQueueRunnerFunction<T>;
/**
* Deferred with a promise resolved/rejected with the result of the function.
*/
deferred: PromiseDefer<T>;
};
/**
* Options to pass to add item.
*/
export type CoreQueueRunnerAddOptions = {
/**
* Whether to allow having multiple entries with same ID in the queue.
*/
allowRepeated?: boolean;
};
/**
* A queue to prevent having too many concurrent executions.
*/
export class CoreQueueRunner {
protected queue: {[id: string]: CoreQueueRunnerItem} = {};
protected orderedQueue: CoreQueueRunnerItem[] = [];
protected numberRunning = 0;
constructor(protected maxParallel: number = 1) { }
/**
* Get unique ID.
*
* @param id ID.
* @return Unique ID.
*/
protected getUniqueId(id: string): string {
let newId = id;
let num = 1;
do {
newId = id + '-' + num;
num++;
} while (newId in this.queue);
return newId;
}
/**
* Process next item in the queue.
*
* @return Promise resolved when next item has been treated.
*/
protected async processNextItem(): Promise<void> {
if (!this.orderedQueue.length || this.numberRunning >= this.maxParallel) {
// Queue is empty or max number of parallel runs reached, stop.
return;
}
const item = this.orderedQueue.shift();
this.numberRunning++;
try {
const result = await item.fn();
item.deferred.resolve(result);
} catch (error) {
item.deferred.reject(error);
} finally {
delete this.queue[item.id];
this.numberRunning--;
this.processNextItem();
}
}
/**
* Add an item to the queue.
*
* @param id ID.
* @param fn Function to call.
* @param options Options.
* @return Promise resolved when the function has been executed.
*/
run<T>(id: string, fn: CoreQueueRunnerFunction<T>, options?: CoreQueueRunnerAddOptions): Promise<T> {
options = options || {};
if (id in this.queue) {
if (!options.allowRepeated) {
// Item already in queue, return its promise.
return this.queue[id].deferred.promise;
}
id = this.getUniqueId(id);
}
// Add the item in the queue.
const item = {
id,
fn,
deferred: CoreUtils.instance.promiseDefer<T>(),
};
this.queue[id] = item;
this.orderedQueue.push(item);
// Process next item if we haven't reached the max yet.
this.processNextItem();
return item.deferred.promise;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -12,9 +12,32 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injector } from '@angular/core';
import { Injector, NgZone as NgZoneService } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { SplashScreen as SplashScreenPlugin } from '@ionic-native/splash-screen/ngx';
import { Platform as PlatformService } from '@ionic/angular';
import { Clipboard as ClipboardService } from '@ionic-native/clipboard/ngx';
import { Diagnostic as DiagnosticService } from '@ionic-native/diagnostic/ngx';
import { Device as DeviceService } from '@ionic-native/device/ngx';
import { File as FileService } from '@ionic-native/file/ngx';
import { FileOpener as FileOpenerService } from '@ionic-native/file-opener/ngx';
import { FileTransfer as FileTransferService } from '@ionic-native/file-transfer/ngx';
import { Geolocation as GeolocationService } from '@ionic-native/geolocation/ngx';
import { Globalization as GlobalizationService } from '@ionic-native/globalization/ngx';
import { InAppBrowser as InAppBrowserService } from '@ionic-native/in-app-browser/ngx';
import { Keyboard as KeyboardService } from '@ionic-native/keyboard/ngx';
import { LocalNotifications as LocalNotificationsService } from '@ionic-native/local-notifications/ngx';
import { Network as NetworkService } from '@ionic-native/network/ngx';
import { Push as PushService } from '@ionic-native/push/ngx';
import { QRScanner as QRScannerService } from '@ionic-native/qr-scanner/ngx';
import { StatusBar as StatusBarService } from '@ionic-native/status-bar/ngx';
import { SplashScreen as SplashScreenService } from '@ionic-native/splash-screen/ngx';
import { SQLite as SQLiteService } from '@ionic-native/sqlite/ngx';
import { WebIntent as WebIntentService } from '@ionic-native/web-intent/ngx';
import { Zip as ZipService } from '@ionic-native/zip/ngx';
import { TranslateService } from '@ngx-translate/core';
import { CoreSingletonsFactory, CoreInjectionToken, CoreSingletonClass } from '@classes/singletons-factory';
@ -39,4 +62,31 @@ export function makeSingleton<Service>(injectionToken: CoreInjectionToken<Servic
return factory.makeSingleton(injectionToken);
}
export class SplashScreen extends makeSingleton(SplashScreenPlugin) {}
// Convert ionic-native services to singleton.
export class Clipboard extends makeSingleton(ClipboardService) {}
export class Device extends makeSingleton(DeviceService) {}
export class Diagnostic extends makeSingleton(DiagnosticService) {}
export class File extends makeSingleton(FileService) {}
export class FileOpener extends makeSingleton(FileOpenerService) {}
export class FileTransfer extends makeSingleton(FileTransferService) {}
export class Geolocation extends makeSingleton(GeolocationService) {}
export class Globalization extends makeSingleton(GlobalizationService) {}
export class InAppBrowser extends makeSingleton(InAppBrowserService) {}
export class Keyboard extends makeSingleton(KeyboardService) {}
export class LocalNotifications extends makeSingleton(LocalNotificationsService) {}
export class Network extends makeSingleton(NetworkService) {}
export class Push extends makeSingleton(PushService) {}
export class QRScanner extends makeSingleton(QRScannerService) {}
export class StatusBar extends makeSingleton(StatusBarService) {}
export class SplashScreen extends makeSingleton(SplashScreenService) {}
export class SQLite extends makeSingleton(SQLiteService) {}
export class WebIntent extends makeSingleton(WebIntentService) {}
export class Zip extends makeSingleton(ZipService) {}
// Convert some Angular and Ionic injectables to singletons.
export class NgZone extends makeSingleton(NgZoneService) {}
export class Http extends makeSingleton(HttpClient) {}
export class Platform extends makeSingleton(PlatformService) {}
// Convert external libraries injectables.
export class Translate extends makeSingleton(TranslateService) {}

View File

@ -43,6 +43,7 @@
]
}
],
"no-angle-bracket-type-assertion": false,
"no-console": [
true,
"debug",
@ -58,12 +59,15 @@
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-shadowed-variable": false,
"no-switch-case-fall-through": true,
"no-unused-expression": false,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"prefer-for-of": false,
"quotemark": [
true,
"single"
@ -104,7 +108,8 @@
"options": [
"ban-keywords",
"check-format",
"allow-pascal-case"
"allow-pascal-case",
"allow-leading-underscore"
]
},
"whitespace": {