1 /*
2  * Copyright (C) 2013-2018 Daniel Nicoletti <dantti12@gmail.com>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17  */
18 #include "grantleeview_p.h"
19 
20 #include "application.h"
21 #include "context.h"
22 #include "action.h"
23 #include "response.h"
24 #include "config.h"
25 
26 #include <grantlee/metatype.h>
27 #include <grantlee/qtlocalizer.h>
28 #include <grantlee/metatype.h>
29 
30 #include <QString>
31 #include <QDirIterator>
32 #include <QtCore/QLoggingCategory>
33 #include <QTranslator>
34 
35 Q_LOGGING_CATEGORY(CUTELYST_GRANTLEE, "cutelyst.grantlee", QtWarningMsg)
36 
37 using namespace Cutelyst;
38 
39 GRANTLEE_BEGIN_LOOKUP(ParamsMultiMap)
40 return object.value(property);
41 GRANTLEE_END_LOOKUP
42 
GrantleeView(QObject * parent,const QString & name)43 GrantleeView::GrantleeView(QObject *parent, const QString &name) : View(new GrantleeViewPrivate, parent, name)
44 {
45     Q_D(GrantleeView);
46 
47     Grantlee::registerMetaType<ParamsMultiMap>();
48 
49     d->loader = QSharedPointer<Grantlee::FileSystemTemplateLoader>(new Grantlee::FileSystemTemplateLoader);
50 
51     d->engine = new Grantlee::Engine(this);
52     d->engine->addTemplateLoader(d->loader);
53 
54     // Set also the paths from CUTELYST_PLUGINS_DIR env variable as plugin paths of grantlee engine
55     const QByteArrayList dirs = QByteArrayList{ QByteArrayLiteral(CUTELYST_PLUGINS_DIR) } + qgetenv("CUTELYST_PLUGINS_DIR").split(';');
56     for (const QByteArray &dir : dirs) {
57         d->engine->addPluginPath(QString::fromLocal8Bit(dir));
58     }
59 
60     d->engine->addDefaultLibrary(QStringLiteral("grantlee_cutelyst"));
61 
62     auto app = qobject_cast<Application *>(parent);
63     if (app) {
64         // make sure templates can be found on the current directory
65         setIncludePaths({ app->config(QStringLiteral("root")).toString() });
66 
67         // If CUTELYST_VAR is set the template might have become
68         // {{ Cutelyst.req.base }} instead of {{ c.req.base }}
69         d->cutelystVar = app->config(QStringLiteral("CUTELYST_VAR"), QStringLiteral("c")).toString();
70 
71         app->loadTranslations(QStringLiteral("plugin_view_grantlee"));
72     } else {
73         // make sure templates can be found on the current directory
74         setIncludePaths({ QDir::currentPath() });
75     }
76 }
77 
includePaths() const78 QStringList GrantleeView::includePaths() const
79 {
80     Q_D(const GrantleeView);
81     return d->includePaths;
82 }
83 
setIncludePaths(const QStringList & paths)84 void GrantleeView::setIncludePaths(const QStringList &paths)
85 {
86     Q_D(GrantleeView);
87     d->loader->setTemplateDirs(paths);
88     d->includePaths = paths;
89     Q_EMIT changed();
90 }
91 
templateExtension() const92 QString GrantleeView::templateExtension() const
93 {
94     Q_D(const GrantleeView);
95     return d->extension;
96 }
97 
setTemplateExtension(const QString & extension)98 void GrantleeView::setTemplateExtension(const QString &extension)
99 {
100     Q_D(GrantleeView);
101     d->extension = extension;
102     Q_EMIT changed();
103 }
104 
wrapper() const105 QString GrantleeView::wrapper() const
106 {
107     Q_D(const GrantleeView);
108     return d->wrapper;
109 }
110 
setWrapper(const QString & name)111 void GrantleeView::setWrapper(const QString &name)
112 {
113     Q_D(GrantleeView);
114     d->wrapper = name;
115     Q_EMIT changed();
116 }
117 
setCache(bool enable)118 void GrantleeView::setCache(bool enable)
119 {
120     Q_D(GrantleeView);
121 
122     if (enable != d->cache.isNull()) {
123         return; // already enabled
124     }
125 
126     delete d->engine;
127     d->engine = new Grantlee::Engine(this);
128 
129     if (enable) {
130         d->cache = QSharedPointer<Grantlee::CachingLoaderDecorator>(new Grantlee::CachingLoaderDecorator(d->loader));
131         d->engine->addTemplateLoader(d->cache);
132     } else {
133         d->cache.clear();
134         d->engine->addTemplateLoader(d->loader);
135     }
136     Q_EMIT changed();
137 }
138 
engine() const139 Grantlee::Engine *GrantleeView::engine() const
140 {
141     Q_D(const GrantleeView);
142     return d->engine;
143 }
144 
preloadTemplates()145 void GrantleeView::preloadTemplates()
146 {
147     Q_D(GrantleeView);
148 
149     if (!isCaching()) {
150         setCache(true);
151     }
152 
153     const auto includePaths = d->includePaths;
154     for (const QString &includePath : includePaths) {
155         QDirIterator it(includePath, {
156                             QLatin1Char('*') + d->extension
157                         },
158                         QDir::Files | QDir::NoDotAndDotDot,
159                         QDirIterator::Subdirectories);
160         while (it.hasNext()) {
161             QString path = it.next();
162             path.remove(includePath);
163             if (path.startsWith(QLatin1Char('/'))) {
164                 path.remove(0, 1);
165             }
166 
167             if (d->cache->canLoadTemplate(path)) {
168                 d->cache->loadByName(path, d->engine);
169             }
170         }
171     }
172 }
173 
isCaching() const174 bool GrantleeView::isCaching() const
175 {
176     Q_D(const GrantleeView);
177     return !d->cache.isNull();
178 }
179 
render(Context * c) const180 QByteArray GrantleeView::render(Context *c) const
181 {
182     Q_D(const GrantleeView);
183 
184     QByteArray ret;
185     c->setStash(d->cutelystVar, QVariant::fromValue(c));
186     const QVariantHash stash = c->stash();
187     auto it = stash.constFind(QStringLiteral("template"));
188     QString templateFile;
189     if (it != stash.constEnd()) {
190         templateFile = it.value().toString();
191     } else {
192         if (c->action() && !c->action()->reverse().isEmpty()) {
193             templateFile = c->action()->reverse() + d->extension;
194             if (templateFile.startsWith(QLatin1Char('/'))) {
195                 templateFile.remove(0, 1);
196             }
197         }
198 
199         if (templateFile.isEmpty()) {
200             c->error(QStringLiteral("Cannot render template, template name or template stash key not defined"));
201             return ret;
202         }
203     }
204 
205     qCDebug(CUTELYST_GRANTLEE) << "Rendering template" << templateFile;
206 
207     Grantlee::Context gc(stash);
208 
209     auto localizer = QSharedPointer<Grantlee::QtLocalizer>::create(c->locale());
210 
211     auto transIt = d->translators.constFind(c->locale());
212     if (transIt != d->translators.constEnd()) {
213         localizer.data()->installTranslator(transIt.value(), transIt.key().name());
214     }
215 
216     auto catalogIt = d->translationCatalogs.constBegin();
217     while (catalogIt != d->translationCatalogs.constEnd()) {
218         localizer.data()->loadCatalog(catalogIt.value(), catalogIt.key());
219         ++it;
220     }
221 
222     gc.setLocalizer(localizer);
223 
224     Grantlee::Template tmpl = d->engine->loadByName(templateFile);
225     if (tmpl->error() != Grantlee::NoError) {
226         c->res()->setBody(c->translate("Cutelyst::GrantleeView", "Internal server error."));
227         c->error(QLatin1String("Error while rendering template: ") + tmpl->errorString());
228         return ret;
229     }
230 
231     QString content = tmpl->render(&gc);
232     if (tmpl->error() != Grantlee::NoError) {
233         c->res()->setBody(c->translate("Cutelyst::GrantleeView", "Internal server error."));
234         c->error(QLatin1String("Error while rendering template: ") + tmpl->errorString());
235         return ret;
236     }
237 
238     if (!d->wrapper.isEmpty()) {
239         Grantlee::Template wrapper = d->engine->loadByName(d->wrapper);
240         if (tmpl->error() != Grantlee::NoError) {
241             c->res()->setBody(c->translate("Cutelyst::GrantleeView", "Internal server error."));
242             c->error(QLatin1String("Error while rendering template: ") + tmpl->errorString());
243             return ret;
244         }
245 
246         Grantlee::SafeString safeContent(content, true);
247         gc.insert(QStringLiteral("content"), safeContent);
248         content = wrapper->render(&gc);
249 
250         if (wrapper->error() != Grantlee::NoError) {
251             c->res()->setBody(c->translate("Cutelyst::GrantleeView", "Internal server error."));
252             c->error(QLatin1String("Error while rendering template: ") + tmpl->errorString());
253             return ret;
254         }
255     }
256 
257     ret = content.toUtf8();
258     return ret;
259 }
260 
addTranslator(const QLocale & locale,QTranslator * translator)261 void GrantleeView::addTranslator(const QLocale &locale, QTranslator *translator)
262 {
263     Q_D(GrantleeView);
264     Q_ASSERT_X(translator, "add translator to GrantleeView", "invalid QTranslator object");
265     d->translators.insert(locale, translator);
266 }
267 
addTranslator(const QString & locale,QTranslator * translator)268 void GrantleeView::addTranslator(const QString &locale, QTranslator *translator)
269 {
270     addTranslator(QLocale(locale), translator);
271 }
272 
addTranslationCatalog(const QString & path,const QString & catalog)273 void GrantleeView::addTranslationCatalog(const QString &path, const QString &catalog)
274 {
275     Q_D(GrantleeView);
276     Q_ASSERT_X(!path.isEmpty(), "add translation catalog to GrantleeView", "empty path");
277     Q_ASSERT_X(!catalog.isEmpty(), "add translation catalog to GrantleeView", "empty catalog name");
278     d->translationCatalogs.insert(catalog, path);
279 }
280 
addTranslationCatalogs(const QHash<QString,QString> & catalogs)281 void GrantleeView::addTranslationCatalogs(const QHash<QString, QString> &catalogs)
282 {
283     Q_D(GrantleeView);
284     Q_ASSERT_X(!catalogs.empty(), "add translation catalogs to GranteleeView", "empty QHash");
285     d->translationCatalogs.unite(catalogs);
286 }
287 
loadTranslationsFromDir(const QString & filename,const QString & directory,const QString & prefix,const QString & suffix)288 QVector<QLocale> GrantleeView::loadTranslationsFromDir(const QString &filename, const QString &directory, const QString &prefix, const QString &suffix)
289 {
290     QVector<QLocale> locales;
291 
292     if (Q_LIKELY(!filename.isEmpty() && !directory.isEmpty())) {
293         const QDir i18nDir(directory);
294         if (Q_LIKELY(i18nDir.exists())) {
295             const QString _prefix = prefix.isEmpty() ? QStringLiteral(".") : prefix;
296             const QString _suffix = suffix.isEmpty() ? QStringLiteral(".qm") : suffix;
297             const QStringList namesFilter = QStringList({filename + _prefix + QLatin1Char('*') + _suffix});
298             const QFileInfoList tsFiles = i18nDir.entryInfoList(namesFilter, QDir::Files);
299             if (Q_LIKELY(!tsFiles.empty())) {
300                 locales.reserve(tsFiles.size());
301                 for (const QFileInfo &ts : tsFiles) {
302                     const QString fn = ts.fileName();
303                     const int prefIdx = fn.indexOf(_prefix);
304                     const QString locString = fn.mid(prefIdx + _prefix.length(), fn.length() - prefIdx - _suffix.length() - _prefix.length());
305                     QLocale loc(locString);
306                     if (Q_LIKELY(loc.language() != QLocale::C)) {
307                         auto trans = new QTranslator(this);
308                         if (Q_LIKELY(trans->load(loc, filename, _prefix, directory))) {
309                             addTranslator(loc, trans);
310                             locales.append(loc);
311                             qCDebug(CUTELYST_GRANTLEE) << "Loaded translations for locale" << loc << "from" << ts.absoluteFilePath();
312                         } else {
313                             delete trans;
314                             qCWarning(CUTELYST_GRANTLEE) << "Can not load translations for locale" << loc;
315                         }
316                     } else {
317                         qCWarning(CUTELYST_GRANTLEE) << "Can not load translations for invalid locale string" << locString;
318                     }
319                 }
320                 locales.squeeze();
321             } else {
322                 qCWarning(CUTELYST_GRANTLEE) << "Can not find translation files for" << filename << "in directory" << directory;
323             }
324         } else {
325             qCWarning(CUTELYST_GRANTLEE) << "Can not load translations from not existing directory:" << directory;
326         }
327     } else {
328         qCWarning(CUTELYST_GRANTLEE) << "Can not load translations for empty file name or empty path.";
329     }
330 
331     return locales;
332 }
333 
334 #include "moc_grantleeview.cpp"
335