2018-01-31 14:17:41 +00:00
|
|
|
// (C) Copyright 2015 Martin Dougiamas
|
|
|
|
//
|
|
|
|
// 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 { Http, Response } from '@angular/http';
|
2018-03-01 15:55:49 +00:00
|
|
|
import { CoreFileProvider } from '@providers/file';
|
|
|
|
import { CoreFilepoolProvider } from '@providers/filepool';
|
|
|
|
import { CoreLoggerProvider } from '@providers/logger';
|
|
|
|
import { CoreSitesProvider } from '@providers/sites';
|
|
|
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
|
|
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
|
|
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
|
|
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
2018-01-31 14:17:41 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A book chapter inside the toc list.
|
|
|
|
*/
|
|
|
|
export interface AddonModBookTocChapter {
|
|
|
|
/**
|
|
|
|
* ID to identify the chapter.
|
|
|
|
* @type {string}
|
|
|
|
*/
|
|
|
|
id: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Chapter's title.
|
|
|
|
* @type {string}
|
|
|
|
*/
|
|
|
|
title: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The chapter's level.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
|
|
|
level: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map of book contents. For each chapter it has its index URL and the list of paths of the files the chapter has. Each path
|
|
|
|
* is identified by the relative path in the book, and the value is the URL of the file.
|
|
|
|
*/
|
|
|
|
export type AddonModBookContentsMap = {[chapter: string]: {indexUrl?: string, paths: {[path: string]: string}}};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Service that provides some features for books.
|
|
|
|
*/
|
|
|
|
@Injectable()
|
|
|
|
export class AddonModBookProvider {
|
|
|
|
static COMPONENT = 'mmaModBook';
|
|
|
|
|
|
|
|
protected ROOT_CACHE_KEY = 'mmaModBook:';
|
|
|
|
protected logger;
|
|
|
|
|
|
|
|
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
|
|
|
|
private fileProvider: CoreFileProvider, private filepoolProvider: CoreFilepoolProvider, private http: Http,
|
|
|
|
private utils: CoreUtilsProvider, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) {
|
|
|
|
this.logger = logger.getInstance('AddonModBookProvider');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a book by course module ID.
|
|
|
|
*
|
|
|
|
* @param {number} courseId Course ID.
|
|
|
|
* @param {number} cmId Course module ID.
|
|
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
|
|
* @return {Promise<any>} Promise resolved when the book is retrieved.
|
|
|
|
*/
|
|
|
|
getBook(courseId: number, cmId: number, siteId?: string): Promise<any> {
|
|
|
|
return this.getBookByField(courseId, 'coursemodule', cmId, siteId);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a book with key=value. If more than one is found, only the first will be returned.
|
|
|
|
*
|
|
|
|
* @param {number} courseId Course ID.
|
|
|
|
* @param {string} key Name of the property to check.
|
|
|
|
* @param {any} value Value to search.
|
|
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
|
|
* @return {Promise<any>} Promise resolved when the book is retrieved.
|
|
|
|
*/
|
|
|
|
protected getBookByField(courseId: number, key: string, value: any, siteId?: string): Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
|
|
const params = {
|
|
|
|
courseids: [courseId]
|
|
|
|
},
|
|
|
|
preSets = {
|
|
|
|
cacheKey: this.getBookDataCacheKey(courseId)
|
|
|
|
};
|
|
|
|
|
|
|
|
return site.read('mod_book_get_books_by_courses', params, preSets).then((response) => {
|
|
|
|
// Search the book.
|
|
|
|
if (response && response.books) {
|
|
|
|
for (const i in response.books) {
|
|
|
|
const book = response.books[i];
|
|
|
|
if (book[key] == value) {
|
|
|
|
return book;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.reject(null);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get cache key for get book data WS calls.
|
|
|
|
*
|
|
|
|
* @param {number} courseId Course ID.
|
|
|
|
* @return {string} Cache key.
|
|
|
|
*/
|
|
|
|
protected getBookDataCacheKey(courseId: number): string {
|
|
|
|
return this.ROOT_CACHE_KEY + 'book:' + courseId;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets a chapter contents.
|
|
|
|
*
|
|
|
|
* @param {AddonModBookContentsMap} contentsMap Contents map returned by getContentsMap.
|
|
|
|
* @param {string} chapterId Chapter to retrieve.
|
|
|
|
* @param {number} moduleId The module ID.
|
|
|
|
* @return {Promise<string>} Promise resolved with the contents.
|
|
|
|
*/
|
|
|
|
getChapterContent(contentsMap: AddonModBookContentsMap, chapterId: string, moduleId: number): Promise<string> {
|
|
|
|
const indexUrl = contentsMap[chapterId] ? contentsMap[chapterId].indexUrl : undefined,
|
|
|
|
siteId = this.sitesProvider.getCurrentSiteId();
|
|
|
|
let promise;
|
|
|
|
|
|
|
|
if (!indexUrl) {
|
|
|
|
// It shouldn't happen.
|
|
|
|
this.logger.debug('Could not locate the index chapter');
|
|
|
|
|
|
|
|
return Promise.reject(null);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.fileProvider.isAvailable()) {
|
|
|
|
promise = this.filepoolProvider.downloadUrl(siteId, indexUrl, false, AddonModBookProvider.COMPONENT, moduleId);
|
|
|
|
} else {
|
|
|
|
// We return the live URL.
|
|
|
|
return Promise.resolve(this.sitesProvider.getCurrentSite().fixPluginfileURL(indexUrl));
|
|
|
|
}
|
|
|
|
|
|
|
|
return promise.then((url) => {
|
|
|
|
// Fetch the URL content.
|
2018-03-14 10:28:51 +00:00
|
|
|
const promise = this.http.get(url).toPromise();
|
2018-01-31 14:17:41 +00:00
|
|
|
|
2018-03-14 10:28:51 +00:00
|
|
|
return promise.then((response: Response): any => {
|
2018-01-31 14:17:41 +00:00
|
|
|
const content = response.text();
|
|
|
|
if (typeof content !== 'string') {
|
|
|
|
return Promise.reject(null);
|
|
|
|
} else {
|
|
|
|
// Now that we have the content, we update the SRC to point back to the external resource.
|
|
|
|
return this.domUtils.restoreSourcesInHtml(content, contentsMap[chapterId].paths);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert an array of book contents into an object where contents are organized in chapters.
|
|
|
|
* Each chapter has an indexUrl and the list of contents in that chapter.
|
|
|
|
*
|
|
|
|
* @param {any[]} contents The module contents.
|
|
|
|
* @return {AddonModBookContentsMap} Contents map.
|
|
|
|
*/
|
|
|
|
getContentsMap(contents: any[]): AddonModBookContentsMap {
|
|
|
|
const map: AddonModBookContentsMap = {};
|
|
|
|
|
|
|
|
if (!contents) {
|
|
|
|
return map;
|
|
|
|
}
|
|
|
|
|
|
|
|
contents.forEach((content) => {
|
|
|
|
if (this.isFileDownloadable(content)) {
|
|
|
|
let chapter,
|
|
|
|
matches,
|
|
|
|
split,
|
|
|
|
filepathIsChapter,
|
|
|
|
key;
|
|
|
|
|
|
|
|
// Search the chapter number in the filepath.
|
|
|
|
matches = content.filepath.match(/\/(\d+)\//);
|
|
|
|
if (matches && matches[1]) {
|
|
|
|
chapter = matches[1];
|
|
|
|
filepathIsChapter = content.filepath == '/' + chapter + '/';
|
|
|
|
|
|
|
|
// Init the chapter if it's not defined yet.
|
|
|
|
map[chapter] = map[chapter] || { paths: {} };
|
|
|
|
|
|
|
|
if (content.filename == 'index.html' && filepathIsChapter) {
|
|
|
|
// Index of the chapter, set indexUrl of the chapter.
|
|
|
|
map[chapter].indexUrl = content.fileurl;
|
|
|
|
} else {
|
|
|
|
if (filepathIsChapter) {
|
|
|
|
// It's a file in the root folder OR the WS isn't returning the filepath as it should (MDL-53671).
|
|
|
|
// Try to get the path to the file from the URL.
|
|
|
|
split = content.fileurl.split('mod_book/chapter' + content.filepath);
|
|
|
|
key = split[1] || content.filename; // Use filename if we couldn't find the path.
|
|
|
|
} else {
|
|
|
|
// Remove the chapter folder from the path and add the filename.
|
|
|
|
key = content.filepath.replace('/' + chapter + '/', '') + content.filename;
|
|
|
|
}
|
|
|
|
|
|
|
|
map[chapter].paths[this.textUtils.decodeURIComponent(key)] = content.fileurl;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return map;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the first chapter of a book.
|
|
|
|
*
|
|
|
|
* @param {AddonModBookTocChapter[]} chapters The chapters list.
|
|
|
|
* @return {string} The chapter id.
|
|
|
|
*/
|
|
|
|
getFirstChapter(chapters: AddonModBookTocChapter[]): string {
|
|
|
|
if (!chapters || !chapters.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return chapters[0].id;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the next chapter to the given one.
|
|
|
|
*
|
|
|
|
* @param {AddonModBookTocChapter[]} chapters The chapters list.
|
|
|
|
* @param {string} chapterId The current chapter.
|
|
|
|
* @return {string} The next chapter id.
|
|
|
|
*/
|
|
|
|
getNextChapter(chapters: AddonModBookTocChapter[], chapterId: string): string {
|
|
|
|
let next = '0';
|
|
|
|
|
|
|
|
for (let i = 0; i < chapters.length; i++) {
|
|
|
|
if (chapters[i].id == chapterId) {
|
|
|
|
if (typeof chapters[i + 1] != 'undefined') {
|
|
|
|
next = chapters[i + 1].id;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return next;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the previous chapter to the given one.
|
|
|
|
*
|
|
|
|
* @param {AddonModBookTocChapter[]} chapters The chapters list.
|
|
|
|
* @param {string} chapterId The current chapter.
|
|
|
|
* @return {string} The next chapter id.
|
|
|
|
*/
|
|
|
|
getPreviousChapter(chapters: AddonModBookTocChapter[], chapterId: string): string {
|
|
|
|
let previous = '0';
|
|
|
|
|
|
|
|
for (let i = 0; i < chapters.length; i++) {
|
|
|
|
if (chapters[i].id == chapterId) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
previous = chapters[i].id;
|
|
|
|
}
|
|
|
|
|
|
|
|
return previous;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the book toc as an array.
|
|
|
|
*
|
|
|
|
* @param {any[]} contents The module contents.
|
|
|
|
* @return {any[]} The toc.
|
|
|
|
*/
|
|
|
|
getToc(contents: any[]): any[] {
|
|
|
|
if (!contents || !contents.length) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2018-03-13 08:12:30 +00:00
|
|
|
return this.textUtils.parseJSON(contents[0].content, []);
|
2018-01-31 14:17:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the book toc as an array of chapters (not nested).
|
|
|
|
*
|
|
|
|
* @param {any[]} contents The module contents.
|
|
|
|
* @return {AddonModBookTocChapter[]} The toc as a list.
|
|
|
|
*/
|
|
|
|
getTocList(contents: any[]): AddonModBookTocChapter[] {
|
|
|
|
const chapters = [],
|
|
|
|
toc = this.getToc(contents);
|
|
|
|
|
|
|
|
toc.forEach((chapter) => {
|
|
|
|
// Add the chapter to the list.
|
|
|
|
let chapterId = chapter.href.replace('/index.html', '');
|
|
|
|
chapters.push({id: chapterId, title: chapter.title, level: chapter.level});
|
|
|
|
|
|
|
|
if (chapter.subitems) {
|
|
|
|
// Add all the subchapters to the list.
|
|
|
|
chapter.subitems.forEach((subChapter) => {
|
|
|
|
chapterId = subChapter.href.replace('/index.html', '');
|
|
|
|
chapters.push({id: chapterId, title: subChapter.title, level: subChapter.level});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return chapters;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidates book data.
|
|
|
|
*
|
|
|
|
* @param {number} courseId Course ID.
|
|
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
|
|
*/
|
|
|
|
invalidateBookData(courseId: number, siteId?: string): Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
|
|
return site.invalidateWsCacheForKey(this.getBookDataCacheKey(courseId));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidate the prefetched content.
|
|
|
|
*
|
|
|
|
* @param {number} moduleId The module ID.
|
|
|
|
* @param {number} courseId Course ID of the module.
|
|
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
|
|
*/
|
|
|
|
invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<any> {
|
|
|
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
|
|
|
|
|
|
|
const promises = [];
|
|
|
|
|
|
|
|
promises.push(this.invalidateBookData(courseId, siteId));
|
|
|
|
promises.push(this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModBookProvider.COMPONENT, moduleId));
|
|
|
|
promises.push(this.courseProvider.invalidateModule(moduleId, siteId));
|
|
|
|
|
|
|
|
return this.utils.allPromises(promises);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a file is downloadable. The file param must have a 'type' attribute like in core_course_get_contents response.
|
|
|
|
*
|
|
|
|
* @param {any} file File to check.
|
|
|
|
* @return {boolean} Whether it's downloadable.
|
|
|
|
*/
|
|
|
|
isFileDownloadable(file: any): boolean {
|
|
|
|
return file.type === 'file';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return whether or not the plugin is enabled.
|
|
|
|
*
|
|
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
|
|
* @return {Promise<boolean>} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
|
|
|
|
*/
|
|
|
|
isPluginEnabled(siteId?: string): Promise<boolean> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
|
|
return site.canDownloadFiles();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Report a book as being viewed.
|
|
|
|
*
|
|
|
|
* @param {number} id Module ID.
|
|
|
|
* @param {string} chapterId Chapter ID.
|
|
|
|
* @return {Promise<any>} Promise resolved when the WS call is successful.
|
|
|
|
*/
|
|
|
|
logView(id: number, chapterId: string): Promise<any> {
|
2018-03-05 14:19:39 +00:00
|
|
|
const params = {
|
|
|
|
bookid: id,
|
|
|
|
chapterid: chapterId
|
|
|
|
};
|
2018-01-31 14:17:41 +00:00
|
|
|
|
2018-03-05 14:19:39 +00:00
|
|
|
return this.sitesProvider.getCurrentSite().write('mod_book_view_book', params);
|
2018-01-31 14:17:41 +00:00
|
|
|
}
|
|
|
|
}
|