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 }