File indexing completed on 2024-05-12 04:58:11

0001 /* ============================================================
0002 * Falkon - Qt web browser
0003 * Copyright (C) 2010-2017 David Rosca <nowrep@gmail.com>
0004 *
0005 * This program is free software: you can redistribute it and/or modify
0006 * it under the terms of the GNU General Public License as published by
0007 * the Free Software Foundation, either version 3 of the License, or
0008 * (at your option) any later version.
0009 *
0010 * This program is distributed in the hope that it will be useful,
0011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
0012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013 * GNU General Public License for more details.
0014 *
0015 * You should have received a copy of the GNU General Public License
0016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0017 * ============================================================ */
0018 #include "searchenginesmanager.h"
0019 #include "searchenginesdialog.h"
0020 #include "editsearchengine.h"
0021 #include "networkmanager.h"
0022 #include "iconprovider.h"
0023 #include "mainapplication.h"
0024 #include "opensearchreader.h"
0025 #include "settings.h"
0026 #include "qzsettings.h"
0027 #include "webview.h"
0028 #include "sqldatabase.h"
0029 
0030 #include <QNetworkReply>
0031 #include <QMessageBox>
0032 #include <QBuffer>
0033 
0034 #include <QUrlQuery>
0035 
0036 #define ENSURE_LOADED if (!m_settingsLoaded) loadSettings();
0037 
0038 static QIcon iconFromBase64(const QByteArray &data)
0039 {
0040     QIcon image;
0041     QByteArray bArray = QByteArray::fromBase64(data);
0042     QBuffer buffer(&bArray);
0043     buffer.open(QIODevice::ReadOnly);
0044     QDataStream in(&buffer);
0045     in >> image;
0046     buffer.close();
0047 
0048     if (!image.isNull()) {
0049         return image;
0050     }
0051 
0052     return IconProvider::emptyWebIcon();
0053 }
0054 
0055 static QByteArray iconToBase64(const QIcon &icon)
0056 {
0057     QByteArray bArray;
0058     QBuffer buffer(&bArray);
0059     buffer.open(QIODevice::WriteOnly);
0060     QDataStream out(&buffer);
0061     out << icon;
0062     buffer.close();
0063     return bArray.toBase64();
0064 }
0065 
0066 SearchEnginesManager::SearchEnginesManager(QObject* parent)
0067     : QObject(parent)
0068     , m_settingsLoaded(false)
0069     , m_saveScheduled(false)
0070 {
0071     Settings settings;
0072     settings.beginGroup(QSL("SearchEngines"));
0073     m_startingEngineName = settings.value(QSL("activeEngine"), QSL("DuckDuckGo")).toString();
0074     m_defaultEngineName = settings.value(QSL("DefaultEngine"), QSL("DuckDuckGo")).toString();
0075     settings.endGroup();
0076 
0077     connect(this, &SearchEnginesManager::enginesChanged, this, &SearchEnginesManager::scheduleSave);
0078 }
0079 
0080 void SearchEnginesManager::loadSettings()
0081 {
0082     m_settingsLoaded = true;
0083 
0084     QSqlQuery query(SqlDatabase::instance()->database());
0085     query.exec(QSL("SELECT name, icon, url, shortcut, suggestionsUrl, suggestionsParameters, postData FROM search_engines"));
0086 
0087     while (query.next()) {
0088         Engine en;
0089         en.name = query.value(0).toString();
0090         en.icon = iconFromBase64(query.value(1).toByteArray());
0091         en.url = query.value(2).toString();
0092         en.shortcut = query.value(3).toString();
0093         en.suggestionsUrl = query.value(4).toString();
0094         en.suggestionsParameters = query.value(5).toByteArray();
0095         en.postData = query.value(6).toByteArray();
0096 
0097         m_allEngines.append(en);
0098 
0099         if (en.name == m_defaultEngineName) {
0100             m_defaultEngine = en;
0101         }
0102     }
0103 
0104     if (m_allEngines.isEmpty()) {
0105         restoreDefaults();
0106     }
0107 
0108     if (m_defaultEngine.name.isEmpty()) {
0109         m_defaultEngine = m_allEngines[0];
0110     }
0111 }
0112 
0113 SearchEngine SearchEnginesManager::engineForShortcut(const QString &shortcut)
0114 {
0115     Engine returnEngine;
0116 
0117     if (shortcut.isEmpty()) {
0118         return returnEngine;
0119     }
0120 
0121     for (const Engine &en : std::as_const(m_allEngines)) {
0122         if (en.shortcut == shortcut) {
0123             returnEngine = en;
0124             break;
0125         }
0126     }
0127 
0128     return returnEngine;
0129 }
0130 
0131 LoadRequest SearchEnginesManager::searchResult(const Engine &engine, const QString &string)
0132 {
0133     ENSURE_LOADED;
0134 
0135     // GET search engine
0136     if (engine.postData.isEmpty()) {
0137         QByteArray url = engine.url.toUtf8();
0138         url.replace("%s", QUrl::toPercentEncoding(string));
0139 
0140         return LoadRequest(QUrl::fromEncoded(url));
0141     }
0142 
0143     // POST search engine
0144     QByteArray data = engine.postData;
0145     data.replace("%s", QUrl::toPercentEncoding(string));
0146 
0147     return LoadRequest(QUrl::fromEncoded(engine.url.toUtf8()), LoadRequest::PostOperation, data);
0148 }
0149 
0150 LoadRequest SearchEnginesManager::searchResult(const QString &string)
0151 {
0152     ENSURE_LOADED;
0153 
0154     const Engine en = qzSettings->searchWithDefaultEngine ? m_defaultEngine : m_activeEngine;
0155     return searchResult(en, string);
0156 }
0157 
0158 void SearchEnginesManager::restoreDefaults()
0159 {
0160     Engine duck;
0161     duck.name = QSL("DuckDuckGo");
0162     duck.icon = QIcon(QSL(":/icons/sites/duck.png"));
0163     duck.url = QSL("https://duckduckgo.com/?q=%s&t=qupzilla");
0164     duck.shortcut = QSL("d");
0165     duck.suggestionsUrl = QSL("https://ac.duckduckgo.com/ac/?q=%s&type=list");
0166 
0167     Engine sp;
0168     sp.name = QSL("Startpage");
0169     sp.icon = QIcon(QSL(":/icons/sites/startpage.png"));
0170     sp.url = QSL("https://www.startpage.com/sp/search?query=%s&cat=web&pl=opensearch");
0171     sp.shortcut = QSL("sp");
0172     sp.suggestionsUrl = QSL("https://www.startpage.com/osuggestions?q=%s");
0173 
0174     Engine wiki;
0175     wiki.name = QSL("Wikipedia (en)");
0176     wiki.icon = QIcon(QSL(":/icons/sites/wikipedia.png"));
0177     wiki.url = QSL("https://en.wikipedia.org/wiki/Special:Search?search=%s&fulltext=Search");
0178     wiki.shortcut = QSL("w");
0179     wiki.suggestionsUrl = QSL("https://en.wikipedia.org/w/api.php?action=opensearch&search=%s&namespace=0");
0180 
0181     Engine google;
0182     google.name = QSL("Google");
0183     google.icon = QIcon(QSL(":icons/sites/google.png"));
0184     google.url = QSL("https://www.google.com/search?client=falkon&q=%s");
0185     google.shortcut = QSL("g");
0186     google.suggestionsUrl = QSL("https://suggestqueries.google.com/complete/search?output=firefox&q=%s");
0187 
0188     addEngine(duck);
0189     addEngine(sp);
0190     addEngine(wiki);
0191     addEngine(google);
0192 
0193     m_defaultEngine = duck;
0194 
0195     Q_EMIT enginesChanged();
0196 }
0197 
0198 // static
0199 QIcon SearchEnginesManager::iconForSearchEngine(const QUrl &url)
0200 {
0201     QIcon ic = IconProvider::iconForDomain(url);
0202 
0203     if (ic.isNull()) {
0204         ic = QIcon::fromTheme(QSL("edit-find"), QIcon(QSL(":icons/menu/search-icon.svg")));
0205     }
0206 
0207     return ic;
0208 }
0209 
0210 void SearchEnginesManager::engineChangedImage()
0211 {
0212     auto* engine = qobject_cast<OpenSearchEngine*>(sender());
0213 
0214     if (!engine) {
0215         return;
0216     }
0217 
0218     for (Engine e : std::as_const(m_allEngines)) {
0219         if (e.name == engine->name() &&
0220             e.url.contains(engine->searchUrl(QSL("%s")).toString()) &&
0221             !engine->image().isNull()
0222            ) {
0223             int index = m_allEngines.indexOf(e);
0224             if (index != -1) {
0225                 m_allEngines[index].icon = QIcon(QPixmap::fromImage(engine->image()));
0226 
0227                 Q_EMIT enginesChanged();
0228 
0229                 delete engine;
0230                 break;
0231             }
0232         }
0233     }
0234 }
0235 
0236 void SearchEnginesManager::editEngine(const Engine &before, const Engine &after)
0237 {
0238     removeEngine(before);
0239     addEngine(after);
0240 }
0241 
0242 void SearchEnginesManager::addEngine(const Engine &engine)
0243 {
0244     ENSURE_LOADED;
0245 
0246     if (m_allEngines.contains(engine)) {
0247         return;
0248     }
0249 
0250     m_allEngines.append(engine);
0251 
0252     Q_EMIT enginesChanged();
0253 }
0254 
0255 void SearchEnginesManager::addEngineFromForm(const QVariantMap &formData, WebView *view)
0256 {
0257     if (formData.isEmpty())
0258         return;
0259 
0260     const QString method = formData.value(QSL("method")).toString();
0261     bool isPost = method == QL1S("post");
0262 
0263     QUrl actionUrl = formData.value(QSL("action")).toUrl();
0264 
0265     if (actionUrl.isRelative()) {
0266         actionUrl = view->url().resolved(actionUrl);
0267     }
0268 
0269     QUrl parameterUrl = actionUrl;
0270 
0271     if (isPost) {
0272         parameterUrl = QUrl(QSL("http://foo.bar"));
0273     }
0274 
0275     const QString &inputName = formData.value(QSL("inputName")).toString();
0276 
0277     QUrlQuery query(parameterUrl);
0278     query.addQueryItem(inputName, QSL("SEARCH"));
0279 
0280     const QVariantList &inputs = formData.value(QSL("inputs")).toList();
0281     for (const QVariant &pair : inputs) {
0282         const QVariantList &list = pair.toList();
0283         if (list.size() != 2)
0284             continue;
0285 
0286         const QString &name = list.at(0).toString();
0287         const QString &value = list.at(1).toString();
0288 
0289         if (name == inputName || name.isEmpty() || value.isEmpty())
0290             continue;
0291 
0292         query.addQueryItem(name, value);
0293     }
0294 
0295     parameterUrl.setQuery(query);
0296 
0297     if (!isPost) {
0298         actionUrl = parameterUrl;
0299     }
0300 
0301     SearchEngine engine;
0302     engine.name = view->title();
0303     engine.icon = view->icon();
0304     engine.url = QString::fromUtf8(actionUrl.toEncoded());
0305 
0306     if (isPost) {
0307         QByteArray data = parameterUrl.toEncoded(QUrl::RemoveScheme);
0308         engine.postData = data.contains('?') ? data.mid(data.lastIndexOf('?') + 1) : QByteArray();
0309         engine.postData.replace((inputName + QL1S("=SEARCH")).toUtf8(), (inputName + QL1S("=%s")).toUtf8());
0310     } else {
0311         engine.url.replace(inputName + QL1S("=SEARCH"), inputName + QL1S("=%s"));
0312     }
0313 
0314     EditSearchEngine dialog(SearchEnginesDialog::tr("Add Search Engine"), view);
0315     dialog.setName(engine.name);
0316     dialog.setIcon(engine.icon);
0317     dialog.setUrl(engine.url);
0318     dialog.setPostData(QString::fromUtf8(engine.postData));
0319 
0320     if (dialog.exec() != QDialog::Accepted) {
0321         return;
0322     }
0323 
0324     engine.name = dialog.name();
0325     engine.icon = dialog.icon();
0326     engine.url = dialog.url();
0327     engine.shortcut = dialog.shortcut();
0328     engine.postData = dialog.postData().toUtf8();
0329 
0330     if (engine.name.isEmpty() || engine.url.isEmpty()) {
0331         return;
0332     }
0333 
0334     addEngine(engine);
0335 }
0336 
0337 void SearchEnginesManager::addEngine(OpenSearchEngine* engine)
0338 {
0339     ENSURE_LOADED;
0340 
0341     Engine en;
0342     en.name = engine->name();
0343     en.url = engine->searchUrl(QSL("searchstring")).toString().replace(QLatin1String("searchstring"), QLatin1String("%s"));
0344 
0345     if (engine->image().isNull()) {
0346         en.icon = iconForSearchEngine(engine->searchUrl(QString()));
0347     }
0348     else {
0349         en.icon = QIcon(QPixmap::fromImage(engine->image()));
0350     }
0351 
0352     en.suggestionsUrl = engine->getSuggestionsUrl();
0353     en.suggestionsParameters = engine->getSuggestionsParameters();
0354     en.postData = engine->getPostData(QSL("searchstring")).replace("searchstring", "%s");
0355 
0356     addEngine(en);
0357 
0358     connect(engine, &OpenSearchEngine::imageChanged, this, &SearchEnginesManager::engineChangedImage);
0359 }
0360 
0361 void SearchEnginesManager::addEngine(const QUrl &url)
0362 {
0363     ENSURE_LOADED;
0364 
0365     if (!url.isValid()) {
0366         return;
0367     }
0368 
0369     qApp->setOverrideCursor(Qt::WaitCursor);
0370 
0371     QNetworkReply* reply = mApp->networkManager()->get(QNetworkRequest(url));
0372     reply->setParent(this);
0373     connect(reply, &QNetworkReply::finished, this, &SearchEnginesManager::replyFinished);
0374 }
0375 
0376 void SearchEnginesManager::replyFinished()
0377 {
0378     qApp->restoreOverrideCursor();
0379 
0380     auto* reply = qobject_cast<QNetworkReply*>(sender());
0381     if (!reply) {
0382         return;
0383     }
0384 
0385     if (reply->error() != QNetworkReply::NoError) {
0386         reply->close();
0387         reply->deleteLater();
0388         return;
0389     }
0390 
0391     OpenSearchReader reader;
0392     OpenSearchEngine* engine = reader.read(reply);
0393     engine->setNetworkAccessManager(mApp->networkManager());
0394 
0395     reply->close();
0396     reply->deleteLater();
0397 
0398     if (checkEngine(engine)) {
0399         addEngine(engine);
0400         QMessageBox::information(nullptr, tr("Search Engine Added"), tr("Search Engine \"%1\" has been successfully added.").arg(engine->name()));
0401     }
0402 }
0403 
0404 bool SearchEnginesManager::checkEngine(OpenSearchEngine* engine)
0405 {
0406     if (!engine->isValid()) {
0407         QString errorString = tr("Search Engine is not valid!");
0408         QMessageBox::warning(nullptr, tr("Error"), tr("Error while adding Search Engine <br><b>Error Message: </b> %1").arg(errorString));
0409 
0410         return false;
0411     }
0412 
0413     return true;
0414 }
0415 
0416 void SearchEnginesManager::setActiveEngine(const Engine &engine)
0417 {
0418     ENSURE_LOADED;
0419 
0420     if (!m_allEngines.contains(engine)) {
0421         return;
0422     }
0423 
0424     m_activeEngine = engine;
0425     Q_EMIT activeEngineChanged();
0426 }
0427 
0428 void SearchEnginesManager::setDefaultEngine(const SearchEnginesManager::Engine &engine)
0429 {
0430     ENSURE_LOADED;
0431 
0432     if (!m_allEngines.contains(engine)) {
0433         return;
0434     }
0435 
0436     m_defaultEngine = engine;
0437     Q_EMIT defaultEngineChanged();
0438 }
0439 
0440 void SearchEnginesManager::removeEngine(const Engine &engine)
0441 {
0442     ENSURE_LOADED;
0443 
0444     int index = m_allEngines.indexOf(engine);
0445 
0446     if (index < 0) {
0447         return;
0448     }
0449 
0450     QSqlQuery query(SqlDatabase::instance()->database());
0451     query.prepare(QSL("DELETE FROM search_engines WHERE name=? AND url=?"));
0452     query.bindValue(0, engine.name);
0453     query.bindValue(1, engine.url);
0454     query.exec();
0455 
0456     m_allEngines.remove(index);
0457     Q_EMIT enginesChanged();
0458 }
0459 
0460 void SearchEnginesManager::setAllEngines(const QVector<Engine> &engines)
0461 {
0462     ENSURE_LOADED;
0463 
0464     m_allEngines = engines;
0465     Q_EMIT enginesChanged();
0466 }
0467 
0468 QVector<SearchEngine> SearchEnginesManager::allEngines()
0469 {
0470     ENSURE_LOADED;
0471 
0472     return m_allEngines;
0473 }
0474 
0475 void SearchEnginesManager::saveSettings()
0476 {
0477     Settings settings;
0478     settings.beginGroup(QSL("SearchEngines"));
0479     settings.setValue(QSL("activeEngine"), m_activeEngine.name);
0480     settings.setValue(QSL("DefaultEngine"), m_defaultEngine.name);
0481     settings.endGroup();
0482 
0483     if (!m_saveScheduled) {
0484         return;
0485     }
0486 
0487     // Well, this is not the best implementation to do as this is taking some time.
0488     // Actually, it is delaying the quit of app for about a 1 sec on my machine with only
0489     // 5 engines. Another problem is that deleting rows without VACUUM isn't actually freeing
0490     // space in database.
0491     //
0492     // But as long as user is not playing with search engines every run it is acceptable.
0493 
0494     QSqlQuery query(SqlDatabase::instance()->database());
0495     query.exec(QSL("DELETE FROM search_engines"));
0496 
0497     for (const Engine &en : std::as_const(m_allEngines)) {
0498         query.prepare(QSL("INSERT INTO search_engines (name, icon, url, shortcut, suggestionsUrl, suggestionsParameters, postData) VALUES (?, ?, ?, ?, ?, ?, ?)"));
0499         query.addBindValue(en.name);
0500         query.addBindValue(iconToBase64(en.icon));
0501         query.addBindValue(en.url);
0502         query.addBindValue(en.shortcut);
0503         query.addBindValue(en.suggestionsUrl);
0504         query.addBindValue(en.suggestionsParameters);
0505         query.addBindValue(en.postData);
0506 
0507         query.exec();
0508     }
0509 }