/* * Copyright (C) 2015-2018 Département de l'Instruction Publique (DIP-SEM) * * Copyright (C) 2013 Open Education Foundation * * Copyright (C) 2010-2013 Groupement d'Intérêt Public pour * l'Education Numérique en Afrique (GIP ENA) * * This file is part of OpenBoard. * * OpenBoard is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3 of the License, * with a specific linking exception for the OpenSSL project's * "OpenSSL" library (or with modified versions of it that use the * same license as the "OpenSSL" library). * * OpenBoard is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenBoard. If not, see . */ #include "UBForeignObjectsHandler.h" #include #include #include "UBSettings.h" const QString tVideo = "video"; const QString tAudio = "audio"; const QString tImage = "image"; const QString tForeignObject = "foreignObject"; const QString tTeacherGuide = "teacherGuide"; const QString tMedia = "media"; const QString tGroups = "groups"; const QString aHref = "xlink:href"; const QString aType = "ub:type"; const QString aReqExt = "requiredExtensions"; const QString aSrc = "ub:src"; const QString aMediaType = "mediaType"; const QString aRelativePath = "relativePath"; const QString aActionMedia = "ub:actionFirstParameter"; const QString vText = "text"; const QString vReqExt = "http://ns.adobe.com/pdf/1.3/"; const QString wgtSuff = ".wgt"; const QString thumbSuff = ".png"; const QString scanDirs = "audios,images,videos,teacherGuideObjects,widgets"; const QStringList trashFilter = QStringList() << "*.swf"; static QString strIdFrom(const QString &filePath) { if ((filePath).isEmpty()) { return QString(); } QRegExp rx("\\{.(?!.*\\{).*\\}"); if (rx.indexIn(filePath) == -1) { return QString(); } return rx.cap(); } static bool rm_r(const QString &rmPath) { QFileInfo fi(rmPath); if (!fi.exists()) { qDebug() << rmPath << "does not exist"; return false; } else if (fi.isFile()) { if (!QFile::remove(rmPath)) { qDebug() << "can't remove file" << rmPath; return false; } return true; } else if (fi.isDir()) { QFileInfoList fList = QDir(rmPath).entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot); foreach (QFileInfo sub, fList) { rm_r(sub.absoluteFilePath()); } if (!QDir().rmdir(rmPath)) { qDebug() << "can't remove dir" << rmPath; return false; } return true; } return false; } static bool cp_rf(const QString &what, const QString &where) { QFileInfo whatFi(what); QFileInfo whereFi = QFileInfo(where); if (!whatFi.exists()) { qDebug() << what << "does not exist" << Q_FUNC_INFO; return false; } else if (whatFi.isFile()) { QString whereDir = where.section("/", 0, -2, QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep); QString newFilePath = where; if (!whereFi.exists()) { QDir().mkpath(whereDir); } else if (whereFi.isDir()) { newFilePath = whereDir + "/" + whatFi.fileName(); } if (QFile::exists(newFilePath)) { QFile::remove(newFilePath); } if (!QFile::copy(what, newFilePath)) { qDebug() << "can't copy" << what << "to" << where << Q_FUNC_INFO; return false; } return true; } else if (whatFi.isDir()) { if (whereFi.isFile() && whereFi.fileName().toLower() == whatFi.fileName().toLower()) { qDebug() << "can't copy dir" << what << "to file" << where << Q_FUNC_INFO; return false; } else if (whereFi.isDir()) { rm_r(where); } QDir().mkpath(where); QFileInfoList fList = QDir(what).entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot); foreach (QFileInfo sub, fList) { if (!cp_rf(sub.absoluteFilePath(), where + "/" + sub.fileName())) return false; } return true; } return true; } static QString thumbFileNameFrom(const QString &filePath) { if (filePath.isEmpty()) { return QString(); } QString thumbPath = filePath; thumbPath.replace(QRegExp("[\\{\\}]"), "").replace(wgtSuff, thumbSuff); return thumbPath; } QString svgPageName(int pageIndex) { return QString("page%1.svg").arg(pageIndex, 3, 10, QLatin1Char('0')); } static QDomDocument createDomFromSvg(const QString &svgUrl) { Q_ASSERT(QFile::exists(svgUrl)); QString mFoldersXmlStorageName = svgUrl; if (QFileInfo(mFoldersXmlStorageName).exists()) { QDomDocument xmlDom; QFile inFile(mFoldersXmlStorageName); if (inFile.open(QIODevice::ReadOnly)) { QString domString(inFile.readAll()); int errorLine = 0; int errorColumn = 0; QString errorStr; if (xmlDom.setContent(domString, &errorStr, &errorLine, &errorColumn)) { return xmlDom; } else { qDebug() << "Error reading content of " << mFoldersXmlStorageName << endl << "Error:" << inFile.errorString() << "Line:" << errorLine << "Column:" << errorColumn; } inFile.close(); } else { qDebug() << "Error reading" << mFoldersXmlStorageName << endl << "Error:" << inFile.errorString(); } } return QDomDocument(); } class Cleaner { public: void cure(const QUrl &dir) { mCurrentDir = dir.toLocalFile(); cleanTrash(); // Gathering information from svg files QFileInfoList svgInfos = QDir(mCurrentDir).entryInfoList(QStringList() << "*.svg", QDir::NoDotAndDotDot | QDir::Files); foreach (QFileInfo svgInfo, svgInfos) { cureIdsFromSvgDom(createDomFromSvg(svgInfo.absoluteFilePath())); } fitIdsFromFileSystem(); QVector deleteCandidates; findRedundandElements(deleteCandidates); foreach (QString key, deleteCandidates) { QString delPath = mPresentIdsMap.value(key); if (delPath.isNull()) { continue; } else if (delPath.endsWith(wgtSuff)) { //remove corresponding thumb QString thumbPath = thumbFileNameFrom(delPath); //N/C - NNE - 20140417 if (QFile::exists(thumbPath)) { rm_r(thumbPath); } } rm_r(delPath); // Clear parent dir if empty QDir dir(delPath); dir.cdUp(); if (dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty()) { dir.rmdir(dir.absolutePath()); } } qDebug() << "Ok on cure"; } private: void cleanTrash() { QFileInfoList ifs = QDir(mCurrentDir).entryInfoList(trashFilter, QDir::NoDotAndDotDot | QDir::Files); foreach (QFileInfo ifo, ifs) { rm_r(ifo.absoluteFilePath()); } } void cureIdsFromSvgDom(const QDomDocument &dom) { Q_ASSERT(!dom.isNull()); QDomElement nextElement = dom.documentElement().firstChildElement(); while (!nextElement.isNull()) { QString nextTag = nextElement.tagName(); qDebug() << "Tag name of the next parsed element is" << nextTag; if (nextTag == tGroups) { nextElement = nextElement.firstChildElement("group"); } invokeFromText(nextTag, nextElement); nextElement = nextElement.nextSiblingElement(); } } void fitIdsFromFileSystem() { QString absPrefix = mCurrentDir + "/"; QStringList dirsList = scanDirs.split(",", QString::SkipEmptyParts); foreach (QString dirName, dirsList) { QString absPath = absPrefix + dirName; if (!QFile::exists(absPath)) { continue; } fitIdsFromDir(absPath); } } void fitIdsFromDir(const QString &scanDir) { QFileInfoList fileList = QDir(scanDir).entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); foreach (QFileInfo nInfo, fileList) { QString uid = strIdFrom(nInfo.fileName()); if (uid.isNull()) { continue; } mPresentIdsMap.insert(uid, nInfo.absoluteFilePath()); } } void findRedundandElements(QVector &v) { // Taking information from the physical file system QStringList domIds = mDomIdsMap.keys(); QStringList presentIds = mPresentIdsMap.keys(); v.resize(qMax(domIds.count(), presentIds.count())); QVector::iterator it_diff; it_diff=std::set_symmetric_difference(domIds.begin(), domIds.end() , presentIds.begin(), presentIds.end() , v.begin()); v.resize(it_diff - v.begin()); } void invokeFromText(const QString &what, const QDomElement &element) { if (what == tVideo || what == tAudio || what == tImage) { mediaToContainer(element); } else if (what == tForeignObject) { foreingObjectToContainer(element); //N/C - NNE - 20140317 cleanObjectFolder(element); //N/C - NNE - 20140520 //foreign object may referer resource which are not present in the svg addResourceIdToSvg(element); } else if (what == tTeacherGuide) { teacherGuideToContainer(element); } pullActionFromElement(element); } // N/C - NNE - 20140317 : When export, reduce the size of the ubz file void cleanObjectFolder(const QDomElement &element) { //QDomElement preference = element.firstChildElement("ub:preference"); //N/C - NNE - 20141021 QDomNodeList childrenNode = element.elementsByTagName("ub:preference"); QVector objectsIdUsed; for(int i = 0; i < childrenNode.size(); i++){ QDomElement preference = childrenNode.at(i).toElement(); if(!preference.isNull()){ QString value = preference.attribute("value"); int findPos = value.indexOf("objects/"); int endPos; //find all objects used while(findPos != -1){ endPos = value.indexOf("\"", findPos); objectsIdUsed << value.mid(findPos, endPos - findPos); findPos = value.indexOf("objects/", endPos); } } } //N/C - NNE - 20141021 : END QString path = element.attribute(aSrc); QString objectsFolderPath = mCurrentDir + "/" + path + "/objects/"; QDir dir(objectsFolderPath); dir.setFilter(QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot); //then check all files in the objects directory //delete the file not used (not in te objectIdUsed variable) QFileInfoList list = dir.entryInfoList(); for (int i = 0; i < list.size(); i++) { QFileInfo fileInfo = list.at(i); if(!objectsIdUsed.contains("objects/"+fileInfo.fileName())){ QFile(fileInfo.filePath()).remove(); } } } // N/C - NNE - 20140317 : END //N/C - NNE - 20140520 void addResourceIdToSvg(const QDomElement& element) { QDomElement textContent = element.firstChildElement("itemTextContent"); QString value = textContent.text(); int findPos = value.indexOf("images/"); int endPos; //find all objects used while(findPos != -1){ endPos = value.indexOf("\"", findPos); QString path = value.mid(findPos, endPos - findPos); QString uuid = path.split("/").at(1).split(".").at(0); mDomIdsMap.insert(uuid, path); findPos = value.indexOf("images/", endPos); } } //N/C - NNE - 20140520 : END void pullActionFromElement(const QDomElement &element) { if (!element.hasAttribute(aActionMedia)) { return; } QString path = element.attribute(aActionMedia); if (path.isNull()) { return; } QString uid = strIdFrom(path); if (uid.isNull()) { return; } mDomIdsMap.insert(uid, path); } void teacherGuideToContainer(const QDomElement &element) { QDomElement nMediaElement = element.firstChildElement(tMedia); while (!nMediaElement.isNull()) { QString path = nMediaElement.attribute(aRelativePath); if (path.isNull()) { continue; } QString uid = strIdFrom(path); if (uid.isNull()) { return; } mDomIdsMap.insert(uid, path); nMediaElement = nMediaElement.nextSiblingElement(tMedia); } } void mediaToContainer(const QDomElement &element) { QString path = element.attribute(aHref); if (path.isNull()) { return; } QString uid = strIdFrom(path); if (uid.isNull()) { return; } mDomIdsMap.insert(uid, path); } void foreingObjectToContainer(const QDomElement &element) { QString type = element.attribute(aType); if (type == vText) { // We don't have to care of the text object return; } QString path = element.attribute(aSrc); if (path.isNull()) { return; } QString uid = strIdFrom(path); if (uid.isNull()) { return; } mDomIdsMap.insert(uid, path); } private: QString mCurrentDir; QDomDocument mSvgData; QMap mDomIdsMap; QMap mPresentIdsMap; }; class PageCopier { public: void copyPage (const QUrl &fromDir, int fromIndex, const QUrl &toDir, int toIndex) { mFromDir = fromDir.toLocalFile(); mToDir = toDir.toLocalFile(); mFromIndex = fromIndex; mToIndex = toIndex; QString svgFrom = mFromDir + "/" + svgPageName(fromIndex); QString svgTo = toDir.toLocalFile() + "/" + svgPageName(toIndex); QDomDocument dd = createDomFromSvg(svgFrom); QFile fl(svgTo); if (!fl.open(QIODevice::WriteOnly)) { qDebug() << Q_FUNC_INFO << "can't open" << fl.fileName() << "for writing"; return; } cureIdsFromSvgDom(dd); QTextStream str(&fl); dd.save(str, 0); fl.close(); qDebug() << Q_FUNC_INFO; } private: void cureIdsFromSvgDom(const QDomDocument &dom) { Q_ASSERT(!dom.isNull()); QDomElement nextElement = dom.documentElement().firstChildElement(); while (!nextElement.isNull( )) { qDebug() << "Tag name of the next parsed element is" << nextElement.tagName(); QString nextTag = nextElement.tagName(); cureFromText(nextTag, nextElement); nextElement = nextElement.nextSiblingElement(); } } void cureFromText(const QString &tagName, QDomElement element) { if (tagName == tVideo || tagName == tAudio || tagName == tImage) { QString newRelative = cureNCopy(element.attribute(aHref)); element.setAttribute(aHref, newRelative); if (element.hasAttribute(aActionMedia)) { QString newActionPath = cureNCopy(element.attribute(aActionMedia)); element.setAttribute(aActionMedia, newActionPath); } } else if (tagName == tForeignObject) { //Pdf object is a special case. Detect if it ends with #reference QString reqExt = element.attribute(aReqExt); if (reqExt == vReqExt) { //pdf reference QString ref = element.attribute(aHref); int i = ref.indexOf("#page"); QString dest = ref.replace(i, ref.length()-i, ""); if (!QFileInfo::exists(dest)) cureNCopy(dest, false); if (ref.isEmpty()) { return; } ref.replace(QRegExp("^(.*pdf\\#page\\=).*$"), QString("\\1%1").arg(mToIndex)); return; } QString type = element.attribute(aType); if (type == vText) { // We don't have to care of the text object if (element.hasAttribute(aActionMedia)) { QString newRelative = cureNCopy(element.attribute(aActionMedia)); element.setAttribute(aActionMedia, newRelative); } return; } QString newRelative = cureNCopy(element.attribute(aSrc)); element.setAttribute(aSrc, newRelative); } else if (tagName == tTeacherGuide) { QDomElement nMediaElement = element.firstChildElement(tMedia); while (!nMediaElement.isNull()) { QString newRelative = cureNCopy(nMediaElement.attribute(aRelativePath)); nMediaElement.setAttribute(aRelativePath, newRelative); nMediaElement = nMediaElement.nextSiblingElement(tMedia); } } } QString cureNCopy(const QString &relativePath, bool createNewUuid=true) { QString relative = relativePath; if (createNewUuid) { QUuid newUuid = QUuid::createUuid(); QString newPath = relative.replace(QRegExp("\\{.*\\}"), newUuid.toString()); cp_rf(mFromDir + "/" + relativePath, mToDir + "/" + newPath); return newPath; } else { cp_rf(mFromDir + "/" + relativePath, mToDir + "/" + relativePath); return relativePath; } } private: QString mFromDir; QString mToDir; int mFromIndex; int mToIndex; }; class UBForeighnObjectsHandlerPrivate { UBForeighnObjectsHandlerPrivate(UBForeighnObjectsHandler *pq) : q(pq) { } public: void cure(const QUrl &dir) { Cleaner *cleaner = new Cleaner; cleaner->cure(dir); delete cleaner; cleaner = 0; } void copyPage (const QUrl &fromDir, int fromIndex, const QUrl &toDir, int toIndex) { PageCopier *copier = new PageCopier; copier->copyPage(fromDir, fromIndex, toDir, toIndex); delete copier; copier = 0; } private: UBForeighnObjectsHandler *q; friend class UBForeighnObjectsHandler; }; UBForeighnObjectsHandler::UBForeighnObjectsHandler() : d(new UBForeighnObjectsHandlerPrivate(this)) { } UBForeighnObjectsHandler::~UBForeighnObjectsHandler() { delete d; } void UBForeighnObjectsHandler::cure(const QList &dirs) { foreach (QUrl dir, dirs) { cure(dir); } } void UBForeighnObjectsHandler::cure(const QUrl &dir) { d->cure(dir); } void UBForeighnObjectsHandler::copyPage(const QUrl &fromDir, int fromIndex, const QUrl &toDir, int toIndex) { d->copyPage(fromDir, fromIndex, toDir, toIndex); }