diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 92b23af69..73abf1fe4 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -14,8 +14,8 @@ jobs: with: node-version: '12.x' - run: npm ci + - run: result=$(find src -type f -iname '*.html' -exec sh -c 'cat {} | tr "\n" " " | grep -Eo "class=\"[^\"]+\"[^>]+class=\"" ' \; | wc -l); test $result -eq 0 - run: npm run lint - run: npx tslint -c ionic-migration.json -p tsconfig.json - run: npm run test:ci - run: npm run build:prod - - run: result=$(find src -type f -iname '*.html' -exec sh -c 'cat {} | tr "\n" " " | grep -Eo "class=\"[^\"]+\"[^>]+class=\"" ' \; | wc -l); test $result -eq 0 diff --git a/src/addons/mod/book/services/book.ts b/src/addons/mod/book/services/book.ts index bc27c60b2..0cf5edb14 100644 --- a/src/addons/mod/book/services/book.ts +++ b/src/addons/mod/book/services/book.ts @@ -376,13 +376,13 @@ export class AddonModBookProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the WS call is successful. */ - logView(id: number, chapterId?: number, name?: string, siteId?: string): Promise { + async logView(id: number, chapterId?: number, name?: string, siteId?: string): Promise { const params: AddonModBookViewBookWSParams = { bookid: id, chapterid: chapterId, }; - return CoreCourseLogHelper.instance.logSingle( + await CoreCourseLogHelper.instance.logSingle( 'mod_book_view_book', params, AddonModBookProvider.COMPONENT, diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index 92edb902e..aafdf2906 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -16,12 +16,14 @@ import { NgModule } from '@angular/core'; import { AddonModBookModule } from './book/book.module'; import { AddonModLessonModule } from './lesson/lesson.module'; +import { AddonModPageModule } from './page/page.module'; @NgModule({ declarations: [], imports: [ AddonModBookModule, AddonModLessonModule, + AddonModPageModule, ], providers: [], exports: [], diff --git a/src/addons/mod/page/components/components.module.ts b/src/addons/mod/page/components/components.module.ts new file mode 100644 index 000000000..c5755973b --- /dev/null +++ b/src/addons/mod/page/components/components.module.ts @@ -0,0 +1,42 @@ +// (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 { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; + +import { AddonModPageIndexComponent } from './index/index'; + +@NgModule({ + declarations: [ + AddonModPageIndexComponent, + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + FormsModule, + CoreSharedModule, + CoreCourseComponentsModule, + ], + exports: [ + AddonModPageIndexComponent, + ], +}) +export class AddonModPageComponentsModule {} diff --git a/src/addons/mod/page/components/index/addon-mod-page-index.html b/src/addons/mod/page/components/index/addon-mod-page-index.html new file mode 100644 index 000000000..3384cd5ac --- /dev/null +++ b/src/addons/mod/page/components/index/addon-mod-page-index.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ {{ 'core.lastmodified' | translate}}: {{ timemodified! * 1000 | coreFormatDate }} +

+
+ +
diff --git a/src/addons/mod/page/components/index/index.scss b/src/addons/mod/page/components/index/index.scss new file mode 100644 index 000000000..f769c305d --- /dev/null +++ b/src/addons/mod/page/components/index/index.scss @@ -0,0 +1,8 @@ +/* Solves iframe height */ +.core-loading-content > div[padding] { + height: 100%; +} + +core-format-text > .no-overflow { + display: inline; +} diff --git a/src/addons/mod/page/components/index/index.ts b/src/addons/mod/page/components/index/index.ts new file mode 100644 index 000000000..09e2c57a1 --- /dev/null +++ b/src/addons/mod/page/components/index/index.ts @@ -0,0 +1,150 @@ +// (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, Optional } from '@angular/core'; +import { + CoreCourseModuleMainResourceComponent, +} from '@features/course/classes/main-resource-component'; +import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; +import { CoreCourse, CoreCourseWSModule } from '@features/course/services/course'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { AddonModPageProvider, AddonModPagePage, AddonModPage } from '../../services/page'; +import { AddonModPageHelper } from '../../services/page-helper'; + +/** + * Component that displays a page. + */ +@Component({ + selector: 'addon-mod-page-index', + templateUrl: 'addon-mod-page-index.html', + styleUrls: ['index.scss'], +}) +export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { + + component = AddonModPageProvider.COMPONENT; + canGetPage = false; + contents?: string; + displayDescription = true; + displayTimemodified = true; + timemodified?: number; + page?: CoreCourseWSModule | AddonModPagePage; + warning?: string; + + protected fetchContentDefaultError = 'addon.mod_page.errorwhileloadingthepage'; + + constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { + super('AddonModPageIndexComponent', courseContentsPage); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + super.ngOnInit(); + + this.canGetPage = AddonModPage.instance.isGetPageWSAvailable(); + + await this.loadContent(); + + try { + await AddonModPage.instance.logView(this.module!.instance!, this.module!.name); + CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata); + } catch { + // Ignore errors. + } + } + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + protected async invalidateContent(): Promise { + await AddonModPage.instance.invalidateContent(this.module!.id, this.courseId!); + } + + /** + * Download page contents. + * + * @param refresh Whether we're refreshing data. + * @return Promise resolved when done. + */ + protected async fetchContent(refresh?: boolean): Promise { + // Download the resource if it needs to be downloaded. + try { + const downloadResult = await this.downloadResourceIfNeeded(refresh); + + const promises: Promise[] = []; + + let getPagePromise: Promise; + + // Get the module to get the latest title and description. Data should've been updated in download. + if (this.canGetPage) { + getPagePromise = AddonModPage.instance.getPageData(this.courseId!, this.module!.id); + } else { + getPagePromise = CoreCourse.instance.getModule(this.module!.id, this.courseId!); + } + + promises.push(getPagePromise.then((page) => { + if (!page) { + return; + } + + this.description = 'intro' in page ? page.intro : page.description; + this.dataRetrieved.emit(page); + + if (!this.canGetPage) { + return; + } + + this.page = page; + + // Check if description and timemodified should be displayed. + if ('displayoptions' in this.page) { + const options: Record = + CoreTextUtils.instance.unserialize(this.page.displayoptions) || {}; + + this.displayDescription = typeof options.printintro == 'undefined' || + CoreUtils.instance.isTrueOrOne(options.printintro); + this.displayTimemodified = typeof options.printlastmodified == 'undefined' || + CoreUtils.instance.isTrueOrOne(options.printlastmodified); + } else { + this.displayDescription = true; + this.displayTimemodified = true; + } + + this.timemodified = 'timemodified' in this.page ? this.page.timemodified : undefined; + + return; + }).catch(() => { + // Ignore errors. + })); + + // Get the page HTML. + promises.push(AddonModPageHelper.instance.getPageHtml(this.module!.contents, this.module!.id).then((content) => { + + this.contents = content; + this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; + + return; + })); + + await Promise.all(promises); + } finally { + this.fillContextMenu(refresh); + } + } + +} diff --git a/src/addons/mod/page/lang.json b/src/addons/mod/page/lang.json new file mode 100644 index 000000000..34bd9817d --- /dev/null +++ b/src/addons/mod/page/lang.json @@ -0,0 +1,4 @@ +{ + "errorwhileloadingthepage": "Error while loading the page content.", + "modulenameplural": "Pages" +} \ No newline at end of file diff --git a/src/addons/mod/page/page-lazy.module.ts b/src/addons/mod/page/page-lazy.module.ts new file mode 100644 index 000000000..759035838 --- /dev/null +++ b/src/addons/mod/page/page-lazy.module.ts @@ -0,0 +1,28 @@ +// (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 { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: ':courseId/:cmdId', + loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModPageIndexPageModule), + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], +}) +export class AddonModPageLazyModule {} diff --git a/src/addons/mod/page/page.module.ts b/src/addons/mod/page/page.module.ts new file mode 100644 index 000000000..eeeeeb12a --- /dev/null +++ b/src/addons/mod/page/page.module.ts @@ -0,0 +1,56 @@ +// (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 { APP_INITIALIZER, NgModule } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; +import { AddonModPageComponentsModule } from './components/components.module'; +import { AddonModPageIndexLinkHandler } from './services/handlers/index-link'; +import { AddonModPageListLinkHandler } from './services/handlers/list-link'; +import { AddonModPageModuleHandler, AddonModPageModuleHandlerService } from './services/handlers/module'; +import { AddonModPagePluginFileHandler } from './services/handlers/pluginfile'; +import { AddonModPagePrefetchHandler } from './services/handlers/prefetch'; + +const routes: Routes = [ + { + path: AddonModPageModuleHandlerService.PAGE_NAME, + loadChildren: () => import('./page-lazy.module').then(m => m.AddonModPageLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + AddonModPageComponentsModule, + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreCourseModuleDelegate.instance.registerHandler(AddonModPageModuleHandler.instance); + CoreContentLinksDelegate.instance.registerHandler(AddonModPageIndexLinkHandler.instance); + CoreContentLinksDelegate.instance.registerHandler(AddonModPageListLinkHandler.instance); + CoreCourseModulePrefetchDelegate.instance.registerHandler(AddonModPagePrefetchHandler.instance); + CorePluginFileDelegate.instance.registerHandler(AddonModPagePluginFileHandler.instance); + }, + }, + ], +}) +export class AddonModPageModule {} diff --git a/src/addons/mod/page/pages/index/index.html b/src/addons/mod/page/pages/index/index.html new file mode 100644 index 000000000..e52bddb1e --- /dev/null +++ b/src/addons/mod/page/pages/index/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/addons/mod/page/pages/index/index.module.ts b/src/addons/mod/page/pages/index/index.module.ts new file mode 100644 index 000000000..3e1036c51 --- /dev/null +++ b/src/addons/mod/page/pages/index/index.module.ts @@ -0,0 +1,46 @@ +// (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 { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModPageComponentsModule } from '../../components/components.module'; +import { AddonModPageIndexPage } from './index'; + +const routes: Routes = [ + { + path: '', + component: AddonModPageIndexPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreSharedModule, + AddonModPageComponentsModule, + ], + declarations: [ + AddonModPageIndexPage, + ], + exports: [RouterModule], +}) +export class AddonModPageIndexPageModule {} diff --git a/src/addons/mod/page/pages/index/index.ts b/src/addons/mod/page/pages/index/index.ts new file mode 100644 index 000000000..93b51ce66 --- /dev/null +++ b/src/addons/mod/page/pages/index/index.ts @@ -0,0 +1,54 @@ +// (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, ViewChild } from '@angular/core'; +import { CoreCourseWSModule } from '@features/course/services/course'; +import { CoreNavigator } from '@services/navigator'; +import { AddonModPageIndexComponent } from '../../components/index/index'; +import { AddonModPagePage } from '../../services/page'; + +/** + * Page that displays a page. + */ +@Component({ + selector: 'page-addon-mod-page-index', + templateUrl: 'index.html', +}) +export class AddonModPageIndexPage implements OnInit { + + @ViewChild(AddonModPageIndexComponent) pageComponent?: AddonModPageIndexComponent; + + title?: string; + module?: CoreCourseWSModule; + courseId?: number; + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.module = CoreNavigator.instance.getRouteParam('module'); + this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId'); + this.title = this.module?.name; + } + + /** + * Update some data based on the page instance. + * + * @param page Page instance. + */ + updateData(page: CoreCourseWSModule | AddonModPagePage): void { + this.title = 'name' in page ? page.name : this.title; + } + +} diff --git a/src/addons/mod/page/services/handlers/index-link.ts b/src/addons/mod/page/services/handlers/index-link.ts new file mode 100644 index 000000000..6cda5a026 --- /dev/null +++ b/src/addons/mod/page/services/handlers/index-link.ts @@ -0,0 +1,44 @@ +// (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 { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; +import { makeSingleton } from '@singletons'; +import { AddonModPage } from '../page'; + +/** + * Handler to treat links to page resource. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModPageIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModPageLinkHandler'; + + constructor() { + super('AddonModPage', 'page', 'p'); + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * + * @param siteId The site ID. + * @return Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string): Promise { + return AddonModPage.instance.isPluginEnabled(siteId); + } + +} + +export class AddonModPageIndexLinkHandler extends makeSingleton(AddonModPageIndexLinkHandlerService) {} diff --git a/src/addons/mod/page/services/handlers/list-link.ts b/src/addons/mod/page/services/handlers/list-link.ts new file mode 100644 index 000000000..22a176266 --- /dev/null +++ b/src/addons/mod/page/services/handlers/list-link.ts @@ -0,0 +1,44 @@ +// (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 { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; +import { makeSingleton } from '@singletons'; +import { AddonModPage } from '../page'; + +/** + * Handler to treat links to page list page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModPageListLinkHandlerService extends CoreContentLinksModuleListHandler { + + name = 'AddonModPageListLinkHandler'; + + constructor() { + super('AddonModPage', 'page'); + } + + /** + * Check if the handler is enabled on a site level. + * + * @param siteId The site ID. + * @return Whether or not the handler is enabled on a site level. + */ + isEnabled(siteId: string): Promise { + return AddonModPage.instance.isPluginEnabled(siteId); + } + +} + +export class AddonModPageListLinkHandler extends makeSingleton(AddonModPageListLinkHandlerService) {} diff --git a/src/addons/mod/page/services/handlers/module.ts b/src/addons/mod/page/services/handlers/module.ts new file mode 100644 index 000000000..dec3d123e --- /dev/null +++ b/src/addons/mod/page/services/handlers/module.ts @@ -0,0 +1,93 @@ +// (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, Type } from '@angular/core'; +import { AddonModPage } from '../page'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { CoreConstants } from '@/core/constants'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { AddonModPageIndexComponent } from '../../components/index'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to support page modules. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModPageModuleHandlerService implements CoreCourseModuleHandler { + + static readonly PAGE_NAME = 'mod_page'; + + name = 'AddonModPage'; + modName = 'page'; + + supportedFeatures = { + [CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE, + [CoreConstants.FEATURE_GROUPS]: false, + [CoreConstants.FEATURE_GROUPINGS]: false, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: false, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: false, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + }; + + /** + * Check if the handler is enabled on a site level. + * + * @return Whether or not the handler is enabled on a site level. + */ + isEnabled(): Promise { + return AddonModPage.instance.isPluginEnabled(); + } + + /** + * Get the data required to display the module in the course contents view. + * + * @param module The module object. + * @return Data to render the module. + */ + getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData { + return { + icon: CoreCourse.instance.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), + title: module.name, + class: 'addon-mod_page-handler', + showDownloadButton: true, + action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void { + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + const routeParams = '/' + courseId + '/' + module.id; + + CoreNavigator.instance.navigateToSitePath(AddonModPageModuleHandlerService.PAGE_NAME + routeParams, options); + }, + }; + } + + /** + * Get the component to render the module. This is needed to support singleactivity course format. + * The component returned must implement CoreCourseModuleMainComponent. + * + * @param course The course object. + * @param module The module object. + * @return The component to use, undefined if not found. + */ + async getMainComponent(): Promise | undefined> { + return AddonModPageIndexComponent; + } + +} +export class AddonModPageModuleHandler extends makeSingleton(AddonModPageModuleHandlerService) {} diff --git a/src/addons/mod/page/services/handlers/pluginfile.ts b/src/addons/mod/page/services/handlers/pluginfile.ts new file mode 100644 index 000000000..7a442ae1f --- /dev/null +++ b/src/addons/mod/page/services/handlers/pluginfile.ts @@ -0,0 +1,63 @@ +// (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 { CorePluginFileHandler } from '@services/plugin-file-delegate'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModPagePluginFileHandlerService implements CorePluginFileHandler { + + name = 'AddonModPagePluginFileHandler'; + component = 'mod_page'; + + /** + * Return the RegExp to match the revision on pluginfile URLs. + * + * @param args Arguments of the pluginfile URL defining component and filearea at least. + * @return RegExp to match the revision on pluginfile URLs. + */ + getComponentRevisionRegExp(args: string[]): RegExp | undefined { + // Check filearea. + if (args[2] == 'content') { + // Component + Filearea + Revision + return new RegExp('/mod_page/content/([0-9]+)/'); + } + } + + /** + * Should return the string to remove the revision on pluginfile url. + * + * @return String to remove the revision on pluginfile url. + */ + getComponentRevisionReplace(): string { + // Component + Filearea + Revision + return '/mod_page/content/0/'; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return Whether or not the handler is enabled on a site level. + */ + async isEnabled(): Promise { + return true; + } + +} + +export class AddonModPagePluginFileHandler extends makeSingleton(AddonModPagePluginFileHandlerService) {} diff --git a/src/addons/mod/page/services/handlers/prefetch.ts b/src/addons/mod/page/services/handlers/prefetch.ts new file mode 100644 index 000000000..36b8bafc6 --- /dev/null +++ b/src/addons/mod/page/services/handlers/prefetch.ts @@ -0,0 +1,90 @@ +// (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 { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler'; +import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton } from '@singletons'; +import { AddonModPage, AddonModPageProvider } from '../page'; + +/** + * Handler to prefetch pages. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModPagePrefetchHandlerService extends CoreCourseResourcePrefetchHandlerBase { + + name = 'AddonModPage'; + modName = 'page'; + component = AddonModPageProvider.COMPONENT; + updatesNames = /^configuration$|^.*files$/; + + /** + * Download or prefetch the content. + * + * @param module The module object returned by WS. + * @param courseId Course ID. + * @param prefetch True to prefetch, false to download right away. + * @return Promise resolved when all content is downloaded. Data returned is not reliable. + */ + async downloadOrPrefetch(module: CoreCourseWSModule, courseId: number, prefetch?: boolean): Promise { + const promises: Promise[] = []; + + promises.push(super.downloadOrPrefetch(module, courseId, prefetch)); + + if (AddonModPage.instance.isGetPageWSAvailable()) { + promises.push(AddonModPage.instance.getPageData(courseId, module.id)); + } + + await Promise.all(promises); + } + + /** + * Invalidate the prefetched content. + * + * @param moduleId The module ID. + * @param courseId Course ID the module belongs to. + * @return Promise resolved when the data is invalidated. + */ + async invalidateContent(moduleId: number, courseId: number): Promise { + await AddonModPage.instance.invalidateContent(moduleId, courseId); + } + + /** + * Invalidate WS calls needed to determine module status. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @return Promise resolved when invalidated. + */ + async invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise { + const promises: Promise[] = []; + + promises.push(AddonModPage.instance.invalidatePageData(courseId)); + promises.push(CoreCourse.instance.invalidateModule(module.id)); + + await CoreUtils.instance.allPromises(promises); + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. + */ + isEnabled(): Promise { + return AddonModPage.instance.isPluginEnabled(); + } + +} +export class AddonModPagePrefetchHandler extends makeSingleton(AddonModPagePrefetchHandlerService) {} diff --git a/src/addons/mod/page/services/page-helper.ts b/src/addons/mod/page/services/page-helper.ts new file mode 100644 index 000000000..b3bb4f58c --- /dev/null +++ b/src/addons/mod/page/services/page-helper.ts @@ -0,0 +1,105 @@ +// (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 { AddonModPageProvider } from './page'; +import { CoreError } from '@classes/errors/error'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreFile } from '@services/file'; +import { CoreSites } from '@services/sites'; +import { CoreFilepool } from '@services/filepool'; +import { CoreWS } from '@services/ws'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton } from '@singletons'; +import { CoreCourseModuleContentFile } from '@features/course/services/course'; + +/** + * Service that provides some features for page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModPageHelperProvider { + + /** + * Gets the page HTML. + * + * @param contents The module contents. + * @param moduleId The module ID. + * @return The HTML of the page. + */ + async getPageHtml(contents: CoreCourseModuleContentFile[], moduleId: number): Promise { + let indexUrl: string | undefined; + const paths: Record = {}; + + // Extract the information about paths from the module contents. + contents.forEach((content) => { + const url = content.fileurl; + + if (this.isMainPage(content)) { + // This seems to be the most reliable way to spot the index page. + indexUrl = url; + } else { + let key = content.filename; + if (content.filepath !== '/') { + // Add the folders without the leading slash. + key = content.filepath.substr(1) + key; + } + paths[CoreTextUtils.instance.decodeURIComponent(key)] = url; + } + }); + + // Promise handling when we are in a browser. + if (!indexUrl) { + // If ever that happens. + throw new CoreError('Could not locate the index page'); + } + + let url: string; + if (CoreFile.instance.isAvailable()) { + // The file system is available. + url = await CoreFilepool.instance.downloadUrl( + CoreSites.instance.getCurrentSiteId(), + indexUrl, + false, + AddonModPageProvider.COMPONENT, + moduleId, + ); + } else { + // We return the live URL. + url = await CoreSites.instance.getCurrentSite()?.checkAndFixPluginfileURL(indexUrl) || ''; + } + + const content = await CoreWS.instance.getText(url); + + // Now that we have the content, we update the SRC to point back to the external resource. + // That will be caught by core-format-text. + return CoreDomUtils.instance.restoreSourcesInHtml(content, paths); + } + + /** + * Returns whether the file is the main page of the module. + * + * @param file An object returned from WS containing file info. + * @return Whether the file is the main page or not. + */ + protected isMainPage(file: CoreCourseModuleContentFile): boolean { + const filename = file.filename || ''; + const fileurl = file.fileurl || ''; + const url = '/mod_page/content/index.html'; + const encodedUrl = encodeURIComponent(url); + + return (filename === 'index.html' && (fileurl.indexOf(url) > 0 || fileurl.indexOf(encodedUrl) > 0 )); + } + +} +export class AddonModPageHelper extends makeSingleton(AddonModPageHelperProvider) {} diff --git a/src/addons/mod/page/services/page.ts b/src/addons/mod/page/services/page.ts new file mode 100644 index 000000000..77955dc3f --- /dev/null +++ b/src/addons/mod/page/services/page.ts @@ -0,0 +1,225 @@ +// (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 { CoreSitesCommonWSOptions, CoreSites } from '@services/sites'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { CoreFilepool } from '@services/filepool'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreWSError } from '@classes/errors/wserror'; + +const ROOT_CACHE_KEY = 'mmaModPage:'; + +/** + * Service that provides some features for page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModPageProvider { + + static readonly COMPONENT = 'mmaModPage'; + + /** + * Get a page by course module ID. + * + * @param courseId Course ID. + * @param cmId Course module ID. + * @param options Other options. + * @return Promise resolved when the page is retrieved. + */ + getPageData(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getPageByKey(courseId, 'coursemodule', cmId, options); + } + + /** + * Get a page. + * + * @param courseId Course ID. + * @param key Name of the property to check. + * @param value Value to search. + * @param options Other options. + * @return Promise resolved when the page is retrieved. + */ + protected async getPageByKey( + courseId: number, + key: string, + value: number, + options: CoreSitesCommonWSOptions = {}, + ): Promise { + const site = await CoreSites.instance.getSite(options.siteId); + + const params: AddonModPageGetPagesByCoursesWSParams = { + courseids: [courseId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getPageCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModPageProvider.COMPONENT, + ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), + }; + + const response = await site.read('mod_page_get_pages_by_courses', params, preSets); + + const currentPage = response.pages.find((page) => page[key] == value); + if (currentPage) { + return currentPage; + } + + throw new CoreWSError('Page not found'); + } + + /** + * Get cache key for page data WS calls. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getPageCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'page:' + courseId; + } + + /** + * Invalidate the prefetched content. + * + * @param moduleId The module ID. + * @param courseId Course ID of the module. + * @param siteId Site ID. If not defined, current site. + */ + invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || CoreSites.instance.getCurrentSiteId(); + + const promises: Promise[] = []; + + promises.push(this.invalidatePageData(courseId, siteId)); + promises.push(CoreFilepool.instance.invalidateFilesByComponent(siteId, AddonModPageProvider.COMPONENT, moduleId)); + promises.push(CoreCourse.instance.invalidateModule(moduleId, siteId)); + + return CoreUtils.instance.allPromises(promises); + } + + /** + * Invalidates page data. + * + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidatePageData(courseId: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getPageCacheKey(courseId)); + } + + /** + * Returns whether or not getPage WS available or not. + * + * @return If WS is avalaible. + * @since 3.3 + */ + isGetPageWSAvailable(): boolean { + return CoreSites.instance.wsAvailableInCurrentSite('mod_page_get_pages_by_courses'); + } + + /** + * Return whether or not the plugin is enabled. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. + */ + async isPluginEnabled(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return site.canDownloadFiles(); + } + + /** + * Report a page as being viewed. + * + * @param pageid Module ID. + * @param name Name of the page. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + logView(pageid: number, name?: string, siteId?: string): Promise { + const params: AddonModPageViewPageWSParams = { + pageid, + }; + + return CoreCourseLogHelper.instance.logSingle( + 'mod_page_view_page', + params, + AddonModPageProvider.COMPONENT, + pageid, + name, + 'page', + {}, + siteId, + ); + } + +} + +export class AddonModPage extends makeSingleton(AddonModPageProvider) {} + + +/** + * Page returned by mod_page_get_pages_by_courses. + */ +export type AddonModPagePage = { + id: number; // Module id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Page name. + intro: string; // Summary. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles: CoreWSExternalFile[]; + content: string; // Page content. + contentformat: number; // Content format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + contentfiles: CoreWSExternalFile[]; + legacyfiles: number; // Legacy files flag. + legacyfileslast: number; // Legacy files last control flag. + display: number; // How to display the page. + displayoptions: string; // Display options (width, height). + revision: number; // Incremented when after each file changes, to avoid cache. + timemodified: number; // Last time the page was modified. + section: number; // Course section id. + visible: number; // Module visibility. + groupmode: number; // Group mode. + groupingid: number; // Grouping id. +}; + +/** + * Result of WS mod_page_get_pages_by_courses. + */ +type AddonModPageGetPagesByCoursesWSResponse = { + pages: AddonModPagePage[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_page_view_page WS. + */ +type AddonModPageViewPageWSParams = { + pageid: number; // Page instance id. +}; + +/** + * Params of mod_page_get_pages_by_courses WS. + */ +type AddonModPageGetPagesByCoursesWSParams = { + courseids?: number[]; // Array of course ids. +}; diff --git a/src/core/services/plugin-file-delegate.ts b/src/core/services/plugin-file-delegate.ts index c5b791374..e7417e778 100644 --- a/src/core/services/plugin-file-delegate.ts +++ b/src/core/services/plugin-file-delegate.ts @@ -310,7 +310,7 @@ export interface CorePluginFileHandler extends CoreDelegateHandler { * @param args Arguments of the pluginfile URL defining component and filearea at least. * @return RegExp to match the revision on pluginfile URLs. */ - getComponentRevisionRegExp?(args: string[]): RegExp; + getComponentRevisionRegExp?(args: string[]): RegExp | undefined; /** * Should return the string to remove the revision on pluginfile url.