Merge pull request #2340 from dpalou/MOBILE-3101

Mobile 3101
main
Juan Leyva 2020-04-27 14:41:56 +02:00 committed by GitHub
commit afae593ed8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 848 additions and 200 deletions

View File

@ -8,6 +8,7 @@
<access launch-external="yes" origin="tel:*" />
<access launch-external="yes" origin="mailto:*" />
<access launch-external="yes" origin="geo:*" />
<allow-navigation href="moodleappfs:*" />
<allow-navigation href="cdvfile:*" />
<allow-navigation href="content:*" />
<allow-navigation href="data:*" />
@ -38,6 +39,8 @@
<preference name="SplashShowOnlyFirstTime" value="false" />
<preference name="LoadUrlTimeoutValue" value="60000" />
<preference name="CustomURLSchemePluginClearsAndroidIntent" value="true" />
<preference name="Scheme" value="moodleappfs" />
<preference name="iosScheme" value="moodleappfs" />
<feature name="StatusBar">
<param name="ios-package" onload="true" value="CDVStatusBar" />
</feature>
@ -171,6 +174,7 @@
<plugin name="cordova-plugin-splashscreen" spec="5.0.3" />
<plugin name="cordova-plugin-statusbar" spec="2.4.3" />
<plugin name="cordova-plugin-whitelist" spec="1.3.4" />
<plugin name="cordova-plugin-wkwebview-cookies" spec="https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git" />
<plugin name="cordova-plugin-zip" spec="3.1.0" />
<plugin name="cordova-sqlite-storage" spec="3.4.0" />
<plugin name="nl.kingsquare.cordova.background-audio" spec="1.0.1" />
@ -178,6 +182,8 @@
<variable name="ANDROID_SUPPORT_V13_VERSION" value="27.+" />
<variable name="FCM_VERSION" value="17.5.+" />
</plugin>
<plugin name="cordova-plugin-ionic-webview" spec="4.1.3" />
<plugin name="cordova-plugin-advanced-http" spec="2.4.1" />
<edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application/activity[@android:name='MainActivity']">
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|screenLayout|smallestScreenSize" android:debuggable="true" />
</edit-config>

View File

@ -12,7 +12,8 @@ const customConfig = {
'@providers': resolve('./src/providers'),
'@components': resolve('./src/components'),
'@directives': resolve('./src/directives'),
'@pipes': resolve('./src/pipes')
'@pipes': resolve('./src/pipes'),
'@singletons': resolve('./src/singletons'),
}
},
externals: [

33
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "moodlemobile",
"version": "3.8.1",
"version": "3.8.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -147,6 +147,11 @@
"resolved": "https://registry.npmjs.org/@ionic-native/globalization/-/globalization-4.20.0.tgz",
"integrity": "sha512-zyxaW+vZb1OHeDgGbrZHQe3hy30K4YeKjGr8KNGcwq+k2ZHkfqo/H6XIwf2m/UlFTgacvdR9XZtfP+6N0suybg=="
},
"@ionic-native/http": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@ionic-native/http/-/http-4.20.0.tgz",
"integrity": "sha512-DF+Y1oYoHTv9Y22a2jLgniOmj9Twba+9j8rzHA4xboVT2HpB6bsBSWOktdAXDVjoajXiLsA/u7fh6YD8//NVGg=="
},
"@ionic-native/in-app-browser": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@ionic-native/in-app-browser/-/in-app-browser-4.20.0.tgz",
@ -1366,7 +1371,8 @@
},
"kind-of": {
"version": "6.0.2",
"resolved": ""
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
}
}
},
@ -2620,6 +2626,11 @@
}
}
},
"cordova-plugin-advanced-http": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/cordova-plugin-advanced-http/-/cordova-plugin-advanced-http-2.4.1.tgz",
"integrity": "sha512-6G8MTy/d02jE6n3Y9CVyCtD5hZGiBb+/dR2AIzhKN1RGGz38g1D2C8yE4MqHRvnmry6k/KHQWT1MsHNXrjouXQ=="
},
"cordova-plugin-badge": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/cordova-plugin-badge/-/cordova-plugin-badge-0.8.8.tgz",
@ -2675,6 +2686,11 @@
"resolved": "https://registry.npmjs.org/cordova-plugin-ionic-keyboard/-/cordova-plugin-ionic-keyboard-2.1.3.tgz",
"integrity": "sha512-6ucQ6FdlLdBm8kJfFnzozmBTjru/0xekHP/dAhjoCZggkGRlgs8TsUJFkxa/bV+qi7Dlo50JjmpE4UMWAO+aOQ=="
},
"cordova-plugin-ionic-webview": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/cordova-plugin-ionic-webview/-/cordova-plugin-ionic-webview-4.1.3.tgz",
"integrity": "sha512-hlrUF0kLjjEkZmpYlLJO0NnXmVjMmQ3MOZVXm1ytDihLPKHklYCOpCvjA5Wz3hJrPD1shFEsqi/SPnp873AsdQ=="
},
"cordova-plugin-local-notification": {
"version": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#0bb96b757fb484553ceabf35a59802f7983a2836",
"from": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle"
@ -2709,6 +2725,10 @@
"resolved": "https://registry.npmjs.org/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.4.tgz",
"integrity": "sha512-EYC5eQFVkoYXq39l7tYKE6lEjHJ04mvTmKXxGL7quHLdFPfJMNzru/UYpn92AOfpl3PQaZmou78C7EgmFOwFQQ=="
},
"cordova-plugin-wkwebview-cookies": {
"version": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git#0f20979803ffd9b13250215975692703342d759c",
"from": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git"
},
"cordova-plugin-zip": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-zip/-/cordova-plugin-zip-3.1.0.tgz",
@ -6672,7 +6692,8 @@
},
"kind-of": {
"version": "6.0.2",
"resolved": "",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
"dev": true
},
"micromatch": {
@ -11323,7 +11344,8 @@
},
"kind-of": {
"version": "6.0.2",
"resolved": ""
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
}
}
},
@ -13484,7 +13506,8 @@
},
"kind-of": {
"version": "6.0.2",
"resolved": "",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
"dev": true
},
"micromatch": {

View File

@ -59,6 +59,7 @@
"@ionic-native/file-transfer": "4.20.0",
"@ionic-native/geolocation": "4.20.0",
"@ionic-native/globalization": "4.20.0",
"@ionic-native/http": "^4.20.0",
"@ionic-native/in-app-browser": "4.20.0",
"@ionic-native/keyboard": "4.20.0",
"@ionic-native/local-notifications": "4.20.0",
@ -81,6 +82,7 @@
"cordova-android-support-gradle-release": "3.0.1",
"cordova-clipboard": "1.3.0",
"cordova-ios": "5.1.1",
"cordova-plugin-advanced-http": "2.4.1",
"cordova-plugin-badge": "0.8.8",
"cordova-plugin-camera": "4.1.0",
"cordova-plugin-customurlscheme": "5.0.0",
@ -92,6 +94,7 @@
"cordova-plugin-globalization": "1.11.0",
"cordova-plugin-inappbrowser": "3.2.0",
"cordova-plugin-ionic-keyboard": "2.1.3",
"cordova-plugin-ionic-webview": "4.1.3",
"cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle",
"cordova-plugin-media-capture": "3.0.3",
"cordova-plugin-network-information": "2.0.2",
@ -99,6 +102,7 @@
"cordova-plugin-splashscreen": "5.0.3",
"cordova-plugin-statusbar": "2.4.3",
"cordova-plugin-whitelist": "1.3.4",
"cordova-plugin-wkwebview-cookies": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git",
"cordova-plugin-zip": "3.1.0",
"cordova-sqlite-storage": "4.0.0",
"cordova-support-google-services": "1.3.2",
@ -185,7 +189,12 @@
},
"cordova-plugin-geolocation": {
"GEOLOCATION_USAGE_DESCRIPTION": "To locate you"
}
},
"cordova-plugin-ionic-webview": {},
"cordova-plugin-advanced-http": {
"OKHTTP_VERSION": "3.10.0"
},
"cordova-plugin-wkwebview-cookies": {}
}
},
"main": "desktop/electron.js",

View File

@ -14,7 +14,7 @@
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule, COMPILER_OPTIONS } from '@angular/core';
import { NgModule, COMPILER_OPTIONS, Injector } from '@angular/core';
import { IonicApp, IonicModule, Platform, Content, ScrollEvent, Config, Refresher } from 'ionic-angular';
import { assert } from 'ionic-angular/util/util';
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
@ -155,6 +155,8 @@ import { AddonQtypeModule } from '@addon/qtype/qtype.module';
import { AddonStorageManagerModule } from '@addon/storagemanager/storagemanager.module';
import { AddonFilterModule } from '@addon/filter/filter.module';
import { setSingletonsInjector } from '@singletons/core.singletons';
// For translate loader. AoT requires an exported function for factories.
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http, './assets/lang/', '.json');
@ -357,6 +359,7 @@ export class AppModule {
private eventsProvider: CoreEventsProvider,
cronDelegate: CoreCronDelegate,
siteInfoCronHandler: CoreSiteInfoCronHandler,
injector: Injector,
) {
// Register a handler for platform ready.
initDelegate.registerProcess({
@ -391,6 +394,9 @@ export class AppModule {
// Register handlers.
cronDelegate.register(siteInfoCronHandler);
// Set the injector.
setSingletonsInjector(injector);
// Set transition animation.
config.setTransition('core-page-transition', CorePageTransition);
config.setTransition('core-modal-lateral-transition', CoreModalLateralTransition);

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,76 @@
// (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 { Injector, Type } from '@angular/core';
/**
* Stub class used to type anonymous classes created in CoreSingletonsFactory#makeSingleton method.
*/
class CoreSingleton {}
/**
* Token that can be used to resolve instances from the injector.
*/
export type CoreInjectionToken<Service> = Type<Service> | Type<any> | string;
/**
* Singleton class created using the factory.
*/
export type CoreSingletonClass<Service> = typeof CoreSingleton & { instance: Service };
/**
* Factory used to create CoreSingleton classes that get instances from an injector.
*/
export class CoreSingletonsFactory {
/**
* Angular injector used to resolve singleton instances.
*/
private injector: Injector;
/**
* Set the injector that will be used to resolve instances in the singletons created with this factory.
*
* @param injector Injector.
*/
setInjector(injector: Injector): void {
this.injector = injector;
}
/**
* Make a singleton that will hold an instance resolved from the factory injector.
*
* @param injectionToken Injection token used to resolve the singleton instance. This is usually the service class if the
* provider was defined using a class or the string used in the `provide` key if it was defined using an object.
*/
makeSingleton<Service>(injectionToken: CoreInjectionToken<Service>): CoreSingletonClass<Service> {
// tslint:disable: no-this-assignment
const factory = this;
return class {
private static _instance: Service;
static get instance(): Service {
// Initialize instances lazily.
if (!this._instance) {
this._instance = factory.injector.get(injectionToken);
}
return this._instance;
}
};
}
}

View File

@ -14,7 +14,6 @@
import { Injector } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
import { SQLiteDB } from './sqlitedb';
import { CoreAppProvider } from '@providers/app';
import { CoreDbProvider } from '@providers/db';
@ -190,7 +189,6 @@ export class CoreSite {
protected domUtils: CoreDomUtilsProvider;
protected eventsProvider: CoreEventsProvider;
protected fileProvider: CoreFileProvider;
protected http: HttpClient;
protected textUtils: CoreTextUtilsProvider;
protected timeUtils: CoreTimeUtilsProvider;
protected translate: TranslateService;
@ -256,7 +254,6 @@ export class CoreSite {
this.domUtils = injector.get(CoreDomUtilsProvider);
this.eventsProvider = injector.get(CoreEventsProvider);
this.fileProvider = injector.get(CoreFileProvider);
this.http = injector.get(HttpClient);
this.textUtils = injector.get(CoreTextUtilsProvider);
this.timeUtils = injector.get(CoreTimeUtilsProvider);
this.translate = injector.get(TranslateService);
@ -1357,55 +1354,62 @@ export class CoreSite {
* @param retrying True if we're retrying the check.
* @return Promise resolved when the check is done.
*/
checkLocalMobilePlugin(retrying?: boolean): Promise<LocalMobileResponse> {
async checkLocalMobilePlugin(retrying?: boolean): Promise<LocalMobileResponse> {
const checkUrl = this.siteUrl + '/local/mobile/check.php',
service = CoreConfigConstants.wsextservice;
if (!service) {
// External service not defined.
return Promise.resolve({ code: 0 });
return { code: 0 };
}
const promise = this.http.post(checkUrl, { service: service }).timeout(this.wsProvider.getRequestTimeout()).toPromise();
let data;
return promise.then((data: any) => {
if (typeof data != 'undefined' && data.errorcode === 'requirecorrectaccess') {
if (!retrying) {
this.siteUrl = this.urlUtils.addOrRemoveWWW(this.siteUrl);
try {
const response = await this.wsProvider.sendHTTPRequest(checkUrl, {
method: 'post',
data: { service: service },
});
return this.checkLocalMobilePlugin(true);
} else {
return Promise.reject(data.error);
}
} else if (typeof data == 'undefined' || typeof data.code == 'undefined') {
// The local_mobile returned something we didn't expect. Let's assume it's not installed.
return { code: 0, warning: 'core.login.localmobileunexpectedresponse' };
}
const code = parseInt(data.code, 10);
if (data.error) {
switch (code) {
case 1:
// Site in maintenance mode.
return Promise.reject(this.translate.instant('core.login.siteinmaintenance'));
case 2:
// Web services not enabled.
return Promise.reject(this.translate.instant('core.login.webservicesnotenabled'));
case 3:
// Extended service not enabled, but the official is enabled.
return { code: 0 };
case 4:
// Neither extended or official services enabled.
return Promise.reject(this.translate.instant('core.login.mobileservicesnotenabled'));
default:
return Promise.reject(this.translate.instant('core.unexpectederror'));
}
} else {
return { code: code, service: service, coreSupported: !!data.coresupported };
}
}, () => {
data = response.body;
} catch (ex) {
return { code: 0 };
});
}
if (typeof data != 'undefined' && data.errorcode === 'requirecorrectaccess') {
if (!retrying) {
this.siteUrl = this.urlUtils.addOrRemoveWWW(this.siteUrl);
return this.checkLocalMobilePlugin(true);
} else {
throw data.error;
}
} else if (typeof data == 'undefined' || typeof data.code == 'undefined') {
// The local_mobile returned something we didn't expect. Let's assume it's not installed.
return { code: 0, warning: 'core.login.localmobileunexpectedresponse' };
}
const code = parseInt(data.code, 10);
if (data.error) {
switch (code) {
case 1:
// Site in maintenance mode.
throw this.translate.instant('core.login.siteinmaintenance');
case 2:
// Web services not enabled.
throw this.translate.instant('core.login.webservicesnotenabled');
case 3:
// Extended service not enabled, but the official is enabled.
return { code: 0 };
case 4:
// Neither extended or official services enabled.
throw this.translate.instant('core.login.mobileservicesnotenabled');
default:
throw this.translate.instant('core.unexpectederror');
}
} else {
return { code: code, service: service, coreSupported: !!data.coresupported };
}
}
/**
@ -1970,7 +1974,7 @@ export class CoreSite {
url = this.fixPluginfileURL(url);
this.tokenPluginFileWorksPromise = this.wsProvider.performHead(url).then((result) => {
return result.ok;
return result.status >= 200 && result.status < 300;
}).catch((error) => {
// Error performing head request.
return false;

View File

@ -16,13 +16,16 @@ import {
Component, Input, Output, OnInit, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange, Optional
} from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { NavController } from 'ionic-angular';
import { NavController, Platform } from 'ionic-angular';
import { CoreFile } from '@providers/file';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreIframeUtilsProvider } from '@providers/utils/iframe';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreUrl } from '@singletons/url';
import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies';
@Component({
selector: 'core-iframe',
@ -49,7 +52,8 @@ export class CoreIframeComponent implements OnInit, OnChanges {
protected navCtrl: NavController,
protected urlUtils: CoreUrlUtilsProvider,
protected utils: CoreUtilsProvider,
@Optional() protected svComponent: CoreSplitViewComponent) {
@Optional() protected svComponent: CoreSplitViewComponent,
protected platform: Platform) {
this.logger = logger.getInstance('CoreIframe');
this.loaded = new EventEmitter<HTMLIFrameElement>();
@ -91,10 +95,28 @@ export class CoreIframeComponent implements OnInit, OnChanges {
/**
* Detect changes on input properties.
*/
ngOnChanges(changes: {[name: string]: SimpleChange }): void {
async ngOnChanges(changes: {[name: string]: SimpleChange }): Promise<void> {
if (changes.src) {
const youtubeUrl = this.urlUtils.getYoutubeEmbedUrl(changes.src.currentValue);
this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(youtubeUrl || changes.src.currentValue);
const url = this.urlUtils.getYoutubeEmbedUrl(changes.src.currentValue) || changes.src.currentValue;
if (this.platform.is('ios')) {
// Save a "fake" cookie for the iframe's domain to fix a bug in WKWebView.
try {
const win = <WKWebViewCookiesWindow> window;
const urlParts = CoreUrl.parse(url);
await win.WKWebViewCookies.setCookie({
name: 'MoodleAppCookieForWKWebView',
value: '1',
domain: urlParts.domain,
});
} catch (err) {
// Ignore errors.
this.logger.error('Error setting cookie', err);
}
}
this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(CoreFile.instance.convertFileSrc(url));
}
}
}

View File

@ -93,5 +93,6 @@
"statusbarbgremotetheme": "#000000",
"statusbarlighttextremotetheme": true,
"enableanalytics": false,
"forceColorScheme": ""
"forceColorScheme": "",
"webviewscheme": "moodleappfs"
}

View File

@ -56,7 +56,7 @@ import { Md5 } from 'ts-md5/dist/md5';
// Import core classes that can be useful for site plugins.
import { CoreSyncBaseProvider } from '@classes/base-sync';
import { CoreUrl } from '@classes/utils/url';
import { CoreUrl } from '@singletons/url';
import { CoreCache } from '@classes/cache';
import { CoreDelegate } from '@classes/delegate';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';

View File

@ -27,6 +27,7 @@ import { CoreH5PUtilsProvider } from './utils';
import { CoreH5PContentValidator } from '../classes/content-validator';
import { TranslateService } from '@ngx-translate/core';
import { FileEntry } from '@ionic-native/file';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Service to provide H5P functionalities.
@ -410,7 +411,7 @@ export class CoreH5PProvider {
* @return Promise resolved with all of the files content in one string.
*/
protected concatenateFiles(assets: CoreH5PDependencyAsset[], type: string): Promise<string> {
const basePath = this.fileProvider.getBasePathInstant();
const basePath = this.fileProvider.convertFileSrc(this.fileProvider.getBasePathInstant());
let content = '',
promise = Promise.resolve(); // Use a chain of promises so the order is kept.
@ -495,7 +496,8 @@ export class CoreH5PProvider {
const contentId = this.getContentId(id),
basePath = this.fileProvider.getBasePathInstant(),
contentUrl = this.textUtils.concatenatePaths(basePath, this.getContentFolderPath(content.folderName, site.getId()));
contentUrl = this.fileProvider.convertFileSrc(this.textUtils.concatenatePaths(
basePath, this.getContentFolderPath(content.folderName, site.getId())));
// Create the settings needed for the content.
const contentSettings = {
@ -560,6 +562,40 @@ export class CoreH5PProvider {
});
}
/**
* Delete all content indexes of all sites from filesystem.
*
* @return Promise resolved when done.
*/
async deleteAllContentIndexes(): Promise<void> {
const siteIds = await this.sitesProvider.getSitesIds();
await Promise.all(siteIds.map((siteId) => this.deleteAllContentIndexesForSite(siteId)));
}
/**
* Delete all content indexes for a certain site from filesystem.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteAllContentIndexesForSite(siteId?: string): Promise<void> {
const site = await this.sitesProvider.getSite(siteId);
const records = await site.getDb().getAllRecords(this.CONTENT_TABLE);
const promises = records.map(async (record) => {
try {
await this.fileProvider.removeFile(this.getContentIndexPath(record.foldername, site.getId()));
} catch (err) {
// Ignore errors, maybe the file doesn't exist.
}
});
await Promise.all(promises);
}
/**
* Delete cached assets from DB and filesystem.
*
@ -1033,12 +1069,11 @@ export class CoreH5PProvider {
settings.loadedJs = [];
settings.loadedCss = [];
const libUrl = this.getCoreH5PPath(),
relPath = this.urlUtils.removeProtocolAndWWW(libUrl);
const libUrl = this.getCoreH5PPath();
// Add core stylesheets.
CoreH5PProvider.STYLES.forEach((style) => {
settings.core.styles.push(relPath + style);
settings.core.styles.push(libUrl + style);
cssRequires.push(libUrl + style);
});
@ -1279,8 +1314,10 @@ export class CoreH5PProvider {
return {
baseUrl: this.fileProvider.getWWWPath(),
url: this.textUtils.concatenatePaths(basePath, this.getExternalH5PFolderPath(site.getId())),
urlLibraries: this.textUtils.concatenatePaths(basePath, this.getLibrariesFolderPath(site.getId())),
url: this.fileProvider.convertFileSrc(this.textUtils.concatenatePaths(
basePath, this.getExternalH5PFolderPath(site.getId()))),
urlLibraries: this.fileProvider.convertFileSrc(this.textUtils.concatenatePaths(
basePath, this.getLibrariesFolderPath(site.getId()))),
postUserStatistics: false,
ajax: ajaxPaths,
saveFreq: false,
@ -2623,6 +2660,8 @@ export class CoreH5PProvider {
}
}
export class CoreH5P extends makeSingleton(CoreH5PProvider) {}
/**
* Display options behaviour constants.
*/

View File

@ -22,7 +22,7 @@ import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreConfigConstants } from '../../../../configconstants';
import { CoreLoginHelperProvider } from '../../providers/helper';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CoreUrl } from '@classes/utils/url';
import { CoreUrl } from '@singletons/url';
import { TranslateService } from '@ngx-translate/core';
/**

View File

@ -16,6 +16,7 @@ import { Directive, Input, AfterViewInit, ElementRef, OnChanges, SimpleChange, O
import { Platform } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreFile } from '@providers/file';
import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
@ -52,9 +53,15 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
invalid = false;
constructor(element: ElementRef, logger: CoreLoggerProvider, private filepoolProvider: CoreFilepoolProvider,
private platform: Platform, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider,
private urlUtils: CoreUrlUtilsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider) {
constructor(element: ElementRef,
logger: CoreLoggerProvider,
protected filepoolProvider: CoreFilepoolProvider,
protected platform: Platform,
protected sitesProvider: CoreSitesProvider,
protected domUtils: CoreDomUtilsProvider,
protected urlUtils: CoreUrlUtilsProvider,
protected appProvider: CoreAppProvider,
protected utils: CoreUtilsProvider) {
// This directive can be added dynamically. In that case, the first param is the HTMLElement.
this.element = element.nativeElement || element;
this.logger = logger.getInstance('CoreExternalContentDirective');
@ -179,7 +186,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
* @param siteId Site ID.
* @return Promise resolved if the element is successfully treated.
*/
protected handleExternalContent(targetAttr: string, url: string, siteId?: string): Promise<any> {
protected async handleExternalContent(targetAttr: string, url: string, siteId?: string): Promise<any> {
const tagName = this.element.tagName;
@ -214,72 +221,70 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
this.addSource(url);
}
return Promise.reject(null);
throw 'Non-downloadable URL';
}
// Get the webservice pluginfile URL, we ignore failures here.
return this.sitesProvider.getSite(siteId).then((site) => {
if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(url)) {
this.element.parentElement.removeChild(this.element); // Remove element since it'll be broken.
const site = await this.sitesProvider.getSite(siteId);
return Promise.reject(null);
if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(url)) {
this.element.parentElement.removeChild(this.element); // Remove element since it'll be broken.
throw 'Site doesn\'t allow downloading files.';
}
// Download images, tracks and posters if size is unknown.
const dwnUnknown = tagName == 'IMG' || tagName == 'TRACK' || targetAttr == 'poster';
let finalUrl: string;
if (targetAttr === 'src' && tagName !== 'SOURCE' && tagName !== 'TRACK' && tagName !== 'VIDEO' && tagName !== 'AUDIO') {
finalUrl = await this.filepoolProvider.getSrcByUrl(siteId, url, this.component, this.componentId, 0, true, dwnUnknown);
} else {
finalUrl = await this.filepoolProvider.getUrlByUrl(siteId, url, this.component, this.componentId, 0, true, dwnUnknown);
finalUrl = CoreFile.instance.convertFileSrc(finalUrl);
}
if (finalUrl.match(/^https?:\/\//i)) {
/* 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 a hash to the URL so both URLs are different. */
finalUrl = finalUrl + '#moodlemobile-embedded';
}
this.logger.debug('Using URL ' + finalUrl + ' for ' + url);
if (tagName === 'SOURCE') {
// The browser does not catch changes in SRC, we need to add a new source.
this.addSource(finalUrl);
} else {
if (tagName === 'IMG') {
this.loaded = false;
this.waitForLoad();
}
this.element.setAttribute(targetAttr, finalUrl);
this.element.setAttribute('data-original-' + targetAttr, url);
}
// Set events to download big files (not downloaded automatically).
if (finalUrl.indexOf('http') === 0 && targetAttr != 'poster' &&
(tagName == 'VIDEO' || tagName == 'AUDIO' || tagName == 'A' || tagName == 'SOURCE')) {
const eventName = tagName == 'A' ? 'click' : 'play';
let clickableEl = this.element;
if (tagName == 'SOURCE') {
clickableEl = <HTMLElement> this.domUtils.closest(this.element, 'video,audio');
if (!clickableEl) {
return;
}
}
// Download images, tracks and posters if size is unknown.
const dwnUnknown = tagName == 'IMG' || tagName == 'TRACK' || targetAttr == 'poster';
let promise;
if (targetAttr === 'src' && tagName !== 'SOURCE' && tagName !== 'TRACK' && tagName !== 'VIDEO' &&
tagName !== 'AUDIO') {
promise = this.filepoolProvider.getSrcByUrl(siteId, url, this.component, this.componentId, 0, true, dwnUnknown);
} else {
promise = this.filepoolProvider.getUrlByUrl(siteId, url, this.component, this.componentId, 0, true, dwnUnknown);
}
return promise.then((finalUrl) => {
if (finalUrl.match(/^https?:\/\//i)) {
/* 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 a hash to the URL so both URLs are different. */
finalUrl = finalUrl + '#moodlemobile-embedded';
}
this.logger.debug('Using URL ' + finalUrl + ' for ' + url);
if (tagName === 'SOURCE') {
// The browser does not catch changes in SRC, we need to add a new source.
this.addSource(finalUrl);
} else {
if (tagName === 'IMG') {
this.loaded = false;
this.waitForLoad();
}
this.element.setAttribute(targetAttr, finalUrl);
this.element.setAttribute('data-original-' + targetAttr, url);
}
// Set events to download big files (not downloaded automatically).
if (finalUrl.indexOf('http') === 0 && targetAttr != 'poster' &&
(tagName == 'VIDEO' || tagName == 'AUDIO' || tagName == 'A' || tagName == 'SOURCE')) {
const eventName = tagName == 'A' ? 'click' : 'play';
let clickableEl = this.element;
if (tagName == 'SOURCE') {
clickableEl = <HTMLElement> this.domUtils.closest(this.element, 'video,audio');
if (!clickableEl) {
return;
}
}
clickableEl.addEventListener(eventName, () => {
// User played media or opened a downloadable link.
// Download the file if in wifi and it hasn't been downloaded already (for big files).
if (this.appProvider.isWifi()) {
// We aren't using the result, so it doesn't matter which of the 2 functions we call.
this.filepoolProvider.getUrlByUrl(siteId, url, this.component, this.componentId, 0, false);
}
});
clickableEl.addEventListener(eventName, () => {
// User played media or opened a downloadable link.
// Download the file if in wifi and it hasn't been downloaded already (for big files).
if (this.appProvider.isWifi()) {
// We aren't using the result, so it doesn't matter which of the 2 functions we call.
this.filepoolProvider.getUrlByUrl(siteId, url, this.component, this.componentId, 0, false);
}
});
});
}
}
/**

View File

@ -74,26 +74,27 @@ export class CoreFormatTextDirective implements OnChanges {
protected loadingChangedListener;
constructor(element: ElementRef,
private sitesProvider: CoreSitesProvider,
private domUtils: CoreDomUtilsProvider,
private textUtils: CoreTextUtilsProvider,
private translate: TranslateService,
private platform: Platform,
private utils: CoreUtilsProvider,
private urlUtils: CoreUrlUtilsProvider,
private loggerProvider: CoreLoggerProvider,
private filepoolProvider: CoreFilepoolProvider,
private appProvider: CoreAppProvider,
private contentLinksHelper: CoreContentLinksHelperProvider,
@Optional() private navCtrl: NavController,
@Optional() private content: Content, @Optional()
private svComponent: CoreSplitViewComponent,
private iframeUtils: CoreIframeUtilsProvider,
private eventsProvider: CoreEventsProvider,
private filterProvider: CoreFilterProvider,
private filterHelper: CoreFilterHelperProvider,
private filterDelegate: CoreFilterDelegate,
private viewContainerRef: ViewContainerRef) {
protected sitesProvider: CoreSitesProvider,
protected domUtils: CoreDomUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected translate: TranslateService,
protected platform: Platform,
protected utils: CoreUtilsProvider,
protected urlUtils: CoreUrlUtilsProvider,
protected loggerProvider: CoreLoggerProvider,
protected filepoolProvider: CoreFilepoolProvider,
protected appProvider: CoreAppProvider,
protected contentLinksHelper: CoreContentLinksHelperProvider,
@Optional() protected navCtrl: NavController,
@Optional() protected content: Content, @Optional()
protected svComponent: CoreSplitViewComponent,
protected iframeUtils: CoreIframeUtilsProvider,
protected eventsProvider: CoreEventsProvider,
protected filterProvider: CoreFilterProvider,
protected filterHelper: CoreFilterHelperProvider,
protected filterDelegate: CoreFilterDelegate,
protected viewContainerRef: ViewContainerRef,
) {
this.element = element.nativeElement;
this.element.classList.add('opacity-hide'); // Hide contents until they're treated.

View File

@ -92,7 +92,7 @@ export class CoreLinkDirective implements OnInit {
protected navigate(href: string): void {
const contentLinksScheme = CoreConfigConstants.customurlscheme + '://link=';
if (href.indexOf('cdvfile://') === 0 || href.indexOf('file://') === 0 || href.indexOf('filesystem:') === 0) {
if (this.urlUtils.isLocalFileUrl(href)) {
// We have a local file.
this.utils.openFile(href).catch((error) => {
this.domUtils.showErrorModal(error);

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>Moodle Desktop</title> <!-- Title is used in desktop, but should be ignored in mobile apps. -->
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="default-src * filesystem: cdvfile: file: data: gap: https://ssl.gstatic.com; img-src * filesystem: gap: data: cdvfile: file: https://ssl.gstatic.com android-webview-video-poster: blob:; style-src 'self' 'unsafe-inline' filesystem: cdvfile: file:; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:* filesystem: cdvfile: file:; media-src * filesystem: cdvfile: file: gap: blob:">
<meta http-equiv="Content-Security-Policy" content="default-src * filesystem: cdvfile: file: data: gap: https://ssl.gstatic.com moodleappfs:; img-src * filesystem: gap: data: cdvfile: file: https://ssl.gstatic.com android-webview-video-poster: blob: moodleappfs:; style-src 'self' 'unsafe-inline' filesystem: cdvfile: file: moodleappfs:; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:* filesystem: cdvfile: file: moodleappfs:; media-src * filesystem: cdvfile: file: gap: blob: moodleappfs:">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">

View File

@ -23,6 +23,7 @@ import { CoreLoggerProvider } from './logger';
import { CoreEventsProvider } from './events';
import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb';
import { CoreConfigConstants } from '../configconstants';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Data stored for a redirect to another page/site.
@ -703,3 +704,5 @@ export class CoreAppProvider {
this.forceOffline = !!value;
}
}
export class CoreApp extends makeSingleton(CoreAppProvider) {}

View File

@ -15,6 +15,7 @@
import { Injectable } from '@angular/core';
import { CoreAppProvider, CoreAppSchema } from './app';
import { SQLiteDB } from '@classes/sqlitedb';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Factory to provide access to dynamic and permanent config and settings.
@ -102,3 +103,5 @@ export class CoreConfigProvider {
return this.appDB.insertRecord(this.TABLE_NAME, { name: name, value: value });
}
}
export class CoreConfig extends makeSingleton(CoreConfigProvider) {}

View File

@ -20,6 +20,7 @@ import { CoreLoggerProvider } from './logger';
import { CoreUtilsProvider } from './utils/utils';
import { CoreConstants } from '@core/constants';
import { SQLiteDB } from '@classes/sqlitedb';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Interface that all cron handlers must implement.
@ -554,3 +555,5 @@ export class CoreCronDelegate {
delete this.handlers[name].timeout;
}
}
export class CoreCron extends makeSingleton(CoreCronDelegate) {}

View File

@ -17,6 +17,7 @@ import { SQLite } from '@ionic-native/sqlite';
import { Platform } from 'ionic-angular';
import { SQLiteDB } from '@classes/sqlitedb';
import { SQLiteDBMock } from '@core/emulator/classes/sqlitedb';
import { makeSingleton } from '@singletons/core.singletons';
/**
* This service allows interacting with the local database to store and retrieve data.
@ -81,3 +82,5 @@ export class CoreDbProvider {
});
}
}
export class CoreDB extends makeSingleton(CoreDbProvider) {}

View File

@ -15,6 +15,7 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { CoreLoggerProvider } from '@providers/logger';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Observer instance to stop listening to an event.
@ -174,3 +175,5 @@ export class CoreEventsProvider {
}
}
}
export class CoreEvents extends makeSingleton(CoreEventsProvider) {}

View File

@ -21,6 +21,7 @@ import { CoreSitesProvider } from './sites';
import { CoreWSProvider } from './ws';
import { CoreUtilsProvider } from './utils/utils';
import { CoreConstants } from '@core/constants';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Provider to provide some helper functions regarding files and packages.
@ -333,5 +334,6 @@ export class CoreFileHelperProvider {
throw new Error('Couldn\'t determine file size: ' + file.fileurl);
}
}
export class CoreFileHelper extends makeSingleton(CoreFileHelperProvider) {}

View File

@ -14,6 +14,7 @@
import { Injectable } from '@angular/core';
import { CoreSitesProvider } from './sites';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Helper to store some temporary data for file submission.
@ -146,3 +147,5 @@ export class CoreFileSessionProvider {
this.files[siteId][component][id] = newFiles;
}
}
export class CoreFileSession extends makeSingleton(CoreFileSessionProvider) {}

View File

@ -20,7 +20,9 @@ import { CoreAppProvider } from './app';
import { CoreLoggerProvider } from './logger';
import { CoreMimetypeUtilsProvider } from './utils/mimetype';
import { CoreTextUtilsProvider } from './utils/text';
import { CoreConfigConstants } from '../configconstants';
import { Zip } from '@ionic-native/zip';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Progress event used when writing a file data into a file.
@ -945,6 +947,7 @@ export class CoreFileProvider {
/**
* Get the internal URL of a file.
* Please notice that with WKWebView these URLs no longer work in mobile. Use fileEntry.toURL() along with convertFileSrc.
*
* @param fileEntry File Entry.
* @return Internal URL.
@ -1269,4 +1272,31 @@ export class CoreFileProvider {
return window.location.href;
}
/**
* Helper function to call Ionic WebView convertFileSrc only in the needed platforms.
* This is needed to make files work with the Ionic WebView plugin.
*
* @param src Source to convert.
* @return Converted src.
*/
convertFileSrc(src: string): string {
return this.appProvider.isMobile() ? (<any> window).Ionic.WebView.convertFileSrc(src) : src;
}
/**
* Undo the conversion of convertFileSrc.
*
* @param src Source to unconvert.
* @return Unconverted src.
*/
unconvertFileSrc(src: string): string {
if (!this.appProvider.isMobile()) {
return src;
}
return src.replace(CoreConfigConstants.webviewscheme + '://localhost/_app_file_', 'file://');
}
}
export class CoreFile extends makeSingleton(CoreFileProvider) {}

View File

@ -31,6 +31,7 @@ import { CoreUtilsProvider } from './utils/utils';
import { SQLiteDB } from '@classes/sqlitedb';
import { CoreConstants } from '@core/constants';
import { Md5 } from 'ts-md5/dist/md5';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Entry from filepool.
@ -471,11 +472,21 @@ export class CoreFilepoolProvider {
protected packagesPromises = {};
protected filePromises: { [s: string]: { [s: string]: Promise<any> } } = {};
constructor(logger: CoreLoggerProvider, private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider,
private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider, private textUtils: CoreTextUtilsProvider,
private utils: CoreUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider, private urlUtils: CoreUrlUtilsProvider,
private timeUtils: CoreTimeUtilsProvider, private eventsProvider: CoreEventsProvider, initDelegate: CoreInitDelegate,
network: Network, private pluginFileDelegate: CorePluginFileDelegate, private domUtils: CoreDomUtilsProvider,
constructor(logger: CoreLoggerProvider,
protected appProvider: CoreAppProvider,
protected fileProvider: CoreFileProvider,
protected sitesProvider: CoreSitesProvider,
protected wsProvider: CoreWSProvider,
protected textUtils: CoreTextUtilsProvider,
protected utils: CoreUtilsProvider,
protected mimeUtils: CoreMimetypeUtilsProvider,
protected urlUtils: CoreUrlUtilsProvider,
protected timeUtils: CoreTimeUtilsProvider,
protected eventsProvider: CoreEventsProvider,
initDelegate: CoreInitDelegate,
network: Network,
protected pluginFileDelegate: CorePluginFileDelegate,
protected domUtils: CoreDomUtilsProvider,
zone: NgZone) {
this.logger = logger.getInstance('CoreFilepoolProvider');
@ -1901,8 +1912,7 @@ export class CoreFilepoolProvider {
if (this.fileProvider.isAvailable()) {
return Promise.resolve(this.getFilePath(siteId, fileId)).then((path) => {
return this.fileProvider.getFile(path).then((fileEntry) => {
// We use toInternalURL so images are loaded in iOS8 using img HTML tags.
return this.fileProvider.getInternalURL(fileEntry);
return this.fileProvider.convertFileSrc(fileEntry.toURL());
});
});
}
@ -3250,3 +3260,5 @@ export class CoreFilepoolProvider {
});
}
}
export class CoreFilepool extends makeSingleton(CoreFilepoolProvider) {}

View File

@ -17,6 +17,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreSitesProvider } from './sites';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Group info for an activity.
@ -449,3 +450,5 @@ export class CoreGroupsProvider {
return groupInfo.defaultGroupId;
}
}
export class CoreGroups extends makeSingleton(CoreGroupsProvider) {}

View File

@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
import { Platform } from 'ionic-angular';
import { CoreLoggerProvider } from './logger';
import { CoreUtilsProvider } from './utils/utils';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Interface that all init handlers must implement.
@ -175,3 +176,5 @@ export class CoreInitDelegate {
this.initProcesses[handler.name] = handler;
}
}
export class CoreInit extends makeSingleton(CoreInitDelegate) {}

View File

@ -20,6 +20,7 @@ import { Platform, Config } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app';
import { CoreConfigProvider } from './config';
import { CoreConfigConstants } from '../configconstants';
import { makeSingleton } from '@singletons/core.singletons';
/*
* Service to handle language features, like changing the current language.
@ -453,3 +454,5 @@ export class CoreLangProvider {
}
}
}
export class CoreLang extends makeSingleton(CoreLangProvider) {}

View File

@ -27,6 +27,7 @@ import { SQLiteDB } from '@classes/sqlitedb';
import { CoreConstants } from '@core/constants';
import { CoreConfigConstants } from '../configconstants';
import { Subject, Subscription } from 'rxjs';
import { makeSingleton } from '@singletons/core.singletons';
/*
* Generated class for the LocalNotificationsProvider provider.
@ -754,3 +755,5 @@ export class CoreLocalNotificationsProvider {
return this.appDB.updateRecords(this.COMPONENTS_TABLE, {id: newId}, {id: oldId});
}
}
export class CoreLocalNotifications extends makeSingleton(CoreLocalNotificationsProvider) {}

View File

@ -14,6 +14,7 @@
import { Injectable } from '@angular/core';
import * as moment from 'moment';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Helper service to display messages in the console.
@ -72,3 +73,5 @@ export class CoreLoggerProvider {
};
}
}
export class CoreLogger extends makeSingleton(CoreLoggerProvider) {}

View File

@ -19,6 +19,7 @@ import { CoreSitesProvider } from './sites';
import { CoreWSExternalFile } from '@providers/ws';
import { FileEntry } from '@ionic-native/file';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Interface that all plugin file handlers must implement.
@ -371,3 +372,5 @@ export class CorePluginFileDelegate extends CoreDelegate {
return Promise.resolve();
}
}
export class CorePluginFile extends makeSingleton(CorePluginFileDelegate) {}

View File

@ -14,6 +14,7 @@
import { Injectable, Injector } from '@angular/core';
import { CoreSite } from '@classes/site';
import { makeSingleton } from '@singletons/core.singletons';
/*
* Provider to create sites instances.
@ -56,3 +57,5 @@ export class CoreSitesFactoryProvider {
return methods;
}
}
export class CoreSitesFactory extends makeSingleton(CoreSitesFactoryProvider) {}

View File

@ -30,6 +30,7 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb';
import { Md5 } from 'ts-md5/dist/md5';
import { WP_PROVIDER } from '@app/app.module';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Response of checking if a site exists and its configuration.
@ -1925,5 +1926,6 @@ export class CoreSitesProvider {
return {};
}
}
}
export class CoreSites extends makeSingleton(CoreSitesProvider) {}

View File

@ -15,6 +15,7 @@
import { Injectable } from '@angular/core';
import { CoreEventsProvider } from './events';
import { CoreSitesProvider, CoreSiteSchema } from './sites';
import { makeSingleton } from '@singletons/core.singletons';
/*
* Service that provides some features regarding synchronization.
@ -206,3 +207,5 @@ export class CoreSyncProvider {
}
}
}
export class CoreSync extends makeSingleton(CoreSyncProvider) {}

View File

@ -17,6 +17,8 @@ import { CoreConfigProvider } from './config';
import { CoreInitHandler, CoreInitDelegate } from './init';
import { CoreLoggerProvider } from './logger';
import { CoreConfigConstants } from '../configconstants';
import { CoreH5P } from '@core/h5p/providers/h5p';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Factory to handle app updates. This factory shouldn't be used outside of core.
@ -33,7 +35,8 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
protected VERSION_APPLIED = 'version_applied';
protected logger;
constructor(logger: CoreLoggerProvider, private configProvider: CoreConfigProvider) {
constructor(logger: CoreLoggerProvider,
protected configProvider: CoreConfigProvider) {
this.logger = logger.getInstance('CoreUpdateManagerProvider');
}
@ -43,19 +46,24 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
*
* @return Promise resolved when the update process finishes.
*/
load(): Promise<any> {
const promises = [],
versionCode = CoreConfigConstants.versioncode;
async load(): Promise<any> {
const promises = [];
const versionCode = CoreConfigConstants.versioncode;
return this.configProvider.get(this.VERSION_APPLIED, 0).then((versionApplied: number) => {
const versionApplied: number = await this.configProvider.get(this.VERSION_APPLIED, 0);
// Put here the code to treat app updates.
if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) {
promises.push(CoreH5P.instance.deleteAllContentIndexes());
}
return Promise.all(promises).then(() => {
return this.configProvider.set(this.VERSION_APPLIED, versionCode);
}).catch((error) => {
this.logger.error(`Error applying update from ${versionApplied} to ${versionCode}`, error);
});
});
try {
await Promise.all(promises);
await this.configProvider.set(this.VERSION_APPLIED, versionCode);
} catch (error) {
this.logger.error(`Error applying update from ${versionApplied} to ${versionCode}`, error);
}
}
}
export class CoreUpdateManager extends makeSingleton(CoreUpdateManagerProvider) {}

View File

@ -28,6 +28,7 @@ import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'
import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins';
import { CoreConfigConstants } from '../configconstants';
import { CoreConstants } from '@core/constants';
import { makeSingleton } from '@singletons/core.singletons';
/**
* All params that can be in a custom URL scheme.
@ -488,3 +489,5 @@ export class CoreCustomURLSchemesProvider {
return url.indexOf(CoreConfigConstants.customurlscheme + '://token=') != -1;
}
}
export class CoreCustomURLSchemes extends makeSingleton(CoreCustomURLSchemesProvider) {}

View File

@ -30,6 +30,7 @@ import { CoreConstants } from '@core/constants';
import { CoreBSTooltipComponent } from '@components/bs-tooltip/bs-tooltip';
import { Md5 } from 'ts-md5/dist/md5';
import { Subject } from 'rxjs';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Interface that defines an extension of the Ionic Alert class, to support multiple listeners.
@ -1666,3 +1667,5 @@ export class CoreDomUtilsProvider {
}, siteId);
}
}
export class CoreDomUtils extends makeSingleton(CoreDomUtilsProvider) {}

View File

@ -25,6 +25,7 @@ import { CoreTextUtilsProvider } from './text';
import { CoreUrlUtilsProvider } from './url';
import { CoreUtilsProvider } from './utils';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { makeSingleton } from '@singletons/core.singletons';
/*
* "Utils" service with helper functions for iframes, embed and similar.
@ -224,7 +225,7 @@ export class CoreIframeUtilsProvider {
} else {
element.setAttribute('src', url);
}
} else if (url.indexOf('cdvfile://') === 0 || url.indexOf('file://') === 0) {
} else if (this.urlUtils.isLocalFileUrl(url)) {
// It's a local file.
this.utils.openFile(url).catch((error) => {
this.domUtils.showErrorModal(error);
@ -352,16 +353,14 @@ export class CoreIframeUtilsProvider {
return;
}
if (scheme && scheme != 'file' && scheme != 'filesystem') {
if (!this.urlUtils.isLocalFileUrlScheme(scheme)) {
// Scheme suggests it's an external resource.
event.preventDefault();
const frameSrc = (<HTMLFrameElement> element).src || (<HTMLObjectElement> element).data,
frameScheme = this.urlUtils.getUrlScheme(frameSrc);
const frameSrc = (<HTMLFrameElement> element).src || (<HTMLObjectElement> element).data;
// If the frame is not local, check the target to identify how to treat the link.
if (frameScheme && frameScheme != 'file' && frameScheme != 'filesystem' &&
(!link.target || link.target == '_self')) {
if (!this.urlUtils.isLocalFileUrl(frameSrc) && (!link.target || link.target == '_self')) {
// Load the link inside the frame itself.
if (element.tagName.toLowerCase() == 'object') {
element.setAttribute('data', link.href);
@ -396,6 +395,8 @@ export class CoreIframeUtilsProvider {
}
}
export class CoreIframeUtils extends makeSingleton(CoreIframeUtilsProvider) {}
/**
* Subtype of HTMLAnchorElement, with some calculated data.
*/

View File

@ -14,9 +14,11 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { CoreFile } from '../file';
import { CoreLoggerProvider } from '../logger';
import { TranslateService } from '@ngx-translate/core';
import { CoreTextUtilsProvider } from './text';
import { makeSingleton } from '@singletons/core.singletons';
/*
* "Utils" service with helper functions for mimetypes and extensions.
@ -164,7 +166,7 @@ export class CoreMimetypeUtilsProvider {
if (this.canBeEmbedded(ext)) {
file.embedType = this.getExtensionType(ext);
path = path || file.fileurl || (file.toURL && file.toURL());
path = CoreFile.instance.convertFileSrc(path || file.fileurl || (file.toURL && file.toURL()));
if (file.embedType == 'image') {
return '<img src="' + path + '">';
@ -549,3 +551,5 @@ export class CoreMimetypeUtilsProvider {
return path;
}
}
export class CoreMimetypeUtils extends makeSingleton(CoreMimetypeUtilsProvider) {}

View File

@ -17,6 +17,7 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ModalController, Platform } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreLangProvider } from '../lang';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Different type of errors the app can treat.
@ -1106,3 +1107,5 @@ export class CoreTextUtilsProvider {
return _unserialize((data + ''), 0)[2];
}
}
export class CoreTextUtils extends makeSingleton(CoreTextUtilsProvider) {}

View File

@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import * as moment from 'moment';
import { CoreConstants } from '@core/constants';
import { makeSingleton } from '@singletons/core.singletons';
/*
* "Utils" service with helper functions for date and time.
@ -353,3 +354,5 @@ export class CoreTimeUtilsProvider {
}
}
}
export class CoreTimeUtils extends makeSingleton(CoreTimeUtilsProvider) {}

View File

@ -15,6 +15,8 @@
import { Injectable } from '@angular/core';
import { CoreLangProvider } from '../lang';
import { CoreTextUtilsProvider } from './text';
import { makeSingleton } from '@singletons/core.singletons';
import { CoreConfigConstants } from '../../configconstants';
/*
* "Utils" service with helper functions for URLs.
@ -423,6 +425,26 @@ export class CoreUrlUtilsProvider {
return /^https?\:\/\/.+/i.test(url);
}
/**
* Check whether an URL belongs to a local file.
*
* @param url URL to check.
* @return Whether the URL belongs to a local file.
*/
isLocalFileUrl(url: string): boolean {
return this.isLocalFileUrlScheme(this.getUrlScheme(url));
}
/**
* Check whether a URL scheme belongs to a local file.
*
* @param scheme Scheme to check.
* @return Whether the scheme belongs to a local file.
*/
isLocalFileUrlScheme(scheme: string): boolean {
return scheme == 'cdvfile' || scheme == 'file' || scheme == 'filesystem' || scheme == CoreConfigConstants.webviewscheme;
}
/**
* Returns if a URL is a pluginfile URL.
*
@ -498,3 +520,5 @@ export class CoreUrlUtilsProvider {
return url;
}
}
export class CoreUrlUtils extends makeSingleton(CoreUrlUtilsProvider) {}

View File

@ -27,6 +27,8 @@ import { CoreLoggerProvider } from '../logger';
import { TranslateService } from '@ngx-translate/core';
import { CoreLangProvider } from '../lang';
import { CoreWSProvider, CoreWSError } from '../ws';
import { CoreFile } from '../file';
import { makeSingleton } from '@singletons/core.singletons';
/**
* Deferred promise. It's similar to the result of $q.defer() in AngularJS.
@ -862,8 +864,11 @@ export class CoreUtilsProvider {
* @return Promise resolved when done.
*/
openFile(path: string): Promise<any> {
const extension = this.mimetypeUtils.getFileExtension(path),
mimetype = this.mimetypeUtils.getMimeType(extension);
// Convert the path to a native path if needed.
path = CoreFile.instance.unconvertFileSrc(path);
const extension = this.mimetypeUtils.getFileExtension(path);
const mimetype = this.mimetypeUtils.getMimeType(extension);
// Path needs to be decoded, the file won't be opened if the path has %20 instead of spaces and so.
try {
@ -1416,3 +1421,5 @@ export class CoreUtilsProvider {
return debounced;
}
}
export class CoreUtils extends makeSingleton(CoreUtilsProvider) {}

View File

@ -24,6 +24,9 @@ import { CoreTextUtilsProvider } from './utils/text';
import { CoreConstants } from '@core/constants';
import { Md5 } from 'ts-md5/dist/md5';
import { CoreInterceptor } from '@classes/interceptor';
import { makeSingleton } from '@singletons/core.singletons';
import { Observable } from 'rxjs/Observable';
import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http';
/**
* PreSets accepted by the WS call.
@ -80,6 +83,61 @@ export interface CoreWSAjaxPreSets {
useGet?: boolean;
}
/**
* Options for HTTP requests.
*/
export type HttpRequestOptions = {
/**
* The HTTP method.
*/
method: string;
/**
* Payload to send to the server. Only applicable on post, put or patch methods.
*/
data?: any;
/**
* Query params to be appended to the URL (only applicable on get, head, delete, upload or download methods).
*/
params?: any;
/**
* Response type. Defaults to json.
*/
responseType?: 'json' | 'text' | 'arraybuffer' | 'blob';
/**
* Timeout for the request in seconds. If undefined, the default value will be used. If null, no timeout.
*/
timeout?: number | null;
/**
* Serializer to use. Defaults to 'urlencoded'. Only for mobile environments.
*/
serializer?: string;
/**
* Whether to follow redirects. Defaults to true. Only for mobile environments.
*/
followRedirect?: boolean;
/**
* Headers. Only for mobile environments.
*/
headers?: {[name: string]: string};
/**
* File paths to use for upload or download. Only for mobile environments.
*/
filePath?: string;
/**
* Name to use during upload. Only for mobile environments.
*/
name?: string;
};
/**
* This service allows performing WS calls and download/upload files.
*/
@ -376,8 +434,8 @@ export class CoreWSProvider {
return Promise.resolve(this.mimeTypeCache[url]);
}
return this.performHead(url).then((data) => {
let mimeType = data.headers.get('Content-Type');
return this.performHead(url).then((response) => {
let mimeType = response.headers.get('Content-Type');
if (mimeType) {
// Remove "parameters" like charset.
mimeType = mimeType.split(';')[0];
@ -398,8 +456,8 @@ export class CoreWSProvider {
* @return Promise resolved with the size or -1 if failure.
*/
getRemoteFileSize(url: string): Promise<number> {
return this.performHead(url).then((data) => {
const size = parseInt(data.headers.get('Content-Length'), 10);
return this.performHead(url).then((response) => {
const size = parseInt(response.headers.get('Content-Length'), 10);
if (size) {
return size;
@ -462,12 +520,12 @@ export class CoreWSProvider {
preSets.responseExpected = true;
}
const script = preSets.noLogin ? 'service-nologin.php' : 'service.php',
ajaxData = JSON.stringify([{
index: 0,
methodname: method,
args: this.convertValuesToString(data)
}]);
const script = preSets.noLogin ? 'service-nologin.php' : 'service.php';
const ajaxData = [{
index: 0,
methodname: method,
args: this.convertValuesToString(data)
}];
// The info= parameter has no function. It is just to help with debugging.
// We call it info to match the parameter name use by Moodle's AMD ajax module.
@ -475,13 +533,22 @@ export class CoreWSProvider {
if (preSets.noLogin && preSets.useGet) {
// Send params using GET.
siteUrl += '&args=' + encodeURIComponent(ajaxData);
promise = this.http.get(siteUrl).timeout(this.getRequestTimeout()).toPromise();
siteUrl += '&args=' + encodeURIComponent(JSON.stringify(ajaxData));
promise = this.sendHTTPRequest(siteUrl, {
method: 'get',
});
} else {
promise = this.http.post(siteUrl, ajaxData).timeout(this.getRequestTimeout()).toPromise();
promise = this.sendHTTPRequest(siteUrl, {
method: 'post',
data: ajaxData,
serializer: 'json',
});
}
return promise.then((data: any) => {
return promise.then((response: HttpResponse<any>) => {
let data = response.body;
// Some moodle web services return null.
// If the responseExpected value is set then so long as no data is returned, we create a blank object.
if (!data && !preSets.responseExpected) {
@ -535,8 +602,11 @@ export class CoreWSProvider {
let promise = this.getPromiseHttp('head', url);
if (!promise) {
promise = this.http.head(url, {observe: 'response', responseType: 'blob'}).timeout(this.getRequestTimeout())
.toPromise();
promise = this.sendHTTPRequest(url, {
method: 'head',
responseType: 'text',
});
promise = this.setPromiseHttp(promise, 'head', url);
}
@ -570,6 +640,7 @@ export class CoreWSProvider {
const promise = this.http.post(requestUrl, ajaxData, options).timeout(this.getRequestTimeout()).toPromise();
return promise.then((data: any) => {
// Some moodle web services return null.
// If the responseExpected value is set to false, we create a blank object if the response is null.
if (!data && !preSets.responseExpected) {
@ -870,15 +941,107 @@ export class CoreWSProvider {
*/
async getText(url: string): Promise<string> {
// Fetch the URL content.
const content = await this.http.get(url, { responseType: 'text' }).toPromise();
const options: HttpRequestOptions = {
method: 'get',
responseType: 'text',
};
const response = await this.sendHTTPRequest(url, options);
const content = response.body;
if (typeof content !== 'string') {
return Promise.reject(null);
throw 'Error reading content';
}
return content;
}
/**
* Send an HTTP request. In mobile devices it will use the cordova plugin.
*
* @param url URL of the request.
* @param options Options for the request.
* @return Promise resolved with the response.
*/
async sendHTTPRequest(url: string, options: HttpRequestOptions): Promise<HttpResponse<any>> {
// Set default values.
options.responseType = options.responseType || 'json';
options.timeout = typeof options.timeout == 'undefined' ? this.getRequestTimeout() : options.timeout;
if (this.appProvider.isMobile()) {
// Use the cordova plugin.
if (url.indexOf('file://') === 0) {
// We cannot load local files using the http native plugin. Use file provider instead.
const format = options.responseType == 'json' ? CoreFileProvider.FORMATJSON : CoreFileProvider.FORMATTEXT;
const content = await this.fileProvider.readFile(url, format);
return new HttpResponse({
body: content,
headers: null,
status: 200,
statusText: 'OK',
url: url
});
}
return new Promise<HttpResponse<any>>((resolve, reject): void => {
// We cannot use Ionic Native plugin because it doesn't have the sendRequest method.
(<any> cordova).plugin.http.sendRequest(url, options, (response) => {
resolve(new CoreNativeToAngularHttpResponse(response));
}, reject);
});
} else {
let observable: Observable<any>;
// Use Angular's library.
switch (options.method) {
case 'get':
observable = this.http.get(url, {
headers: options.headers,
params: options.params,
observe: 'response',
responseType: <any> options.responseType,
});
break;
case 'post':
if (options.serializer == 'json') {
options.data = JSON.stringify(options.data);
}
observable = this.http.post(url, options.data, {
headers: options.headers,
observe: 'response',
responseType: <any> options.responseType,
});
break;
case 'head':
observable = this.http.head(url, {
headers: options.headers,
observe: 'response',
responseType: <any> options.responseType
});
break;
default:
return Promise.reject('Method not implemented yet.');
}
if (options.timeout) {
observable = observable.timeout(options.timeout);
}
return observable.toPromise();
}
}
}
export class CoreWS extends makeSingleton(CoreWSProvider) {}
/**
* Error returned by a WS call.
*/

View File

@ -0,0 +1,49 @@
// (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 { AlertController, App } from 'ionic-angular';
import { Injector } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
import { CoreSingletonsFactory, CoreInjectionToken, CoreSingletonClass } from '@classes/singletons-factory';
const factory = new CoreSingletonsFactory();
/**
* Set the injector that will be used to resolve instances in the singletons of this module.
*
* @param injector Module injector.
*/
export function setSingletonsInjector(injector: Injector): void {
factory.setInjector(injector);
}
/**
* Make a singleton for this module.
*
* @param injectionToken Injection token used to resolve the singleton instance. This is usually the service class if the
* provider was defined using a class or the string used in the `provide` key if it was defined using an object.
*/
export function makeSingleton<Service>(injectionToken: CoreInjectionToken<Service>): CoreSingletonClass<Service> {
return factory.makeSingleton(injectionToken);
}
export class Translate extends makeSingleton(TranslateService) {}
export class Alerts extends makeSingleton(AlertController) {}
export class Ionic extends makeSingleton(App) {}
export class Http extends makeSingleton(HttpClient) {}

View File

@ -21,7 +21,8 @@
"@providers/*": ["providers/*"],
"@components/*": ["components/*"],
"@directives/*": ["directives/*"],
"@pipes/*": ["pipes/*"]
"@pipes/*": ["pipes/*"],
"@singletons/*": ["singletons/*"]
},
"typeRoots": [
"node_modules/@types"