1 /*
2  * articlematcher.cpp
3  *
4  * SPDX-FileCopyrightText: 2004, 2005 Frerich Raabe <raabe@kde.org>
5  *
6  * SPDX-License-Identifier: BSD-2-Clause
7  */
8 #include "articlematcher.h"
9 #include "akregator_debug.h"
10 #include "article.h"
11 #include "types.h"
12 #include <KConfig>
13 #include <KConfigGroup>
14 #include <QUrl>
15 
16 #include <QRegularExpression>
17 
18 namespace Akregator
19 {
20 namespace Filters
21 {
AbstractMatcher()22 AbstractMatcher::AbstractMatcher()
23 {
24 }
25 
~AbstractMatcher()26 AbstractMatcher::~AbstractMatcher()
27 {
28 }
29 
subjectToString(Subject subj)30 QString Criterion::subjectToString(Subject subj)
31 {
32     switch (subj) {
33     case Title:
34         return QStringLiteral("Title");
35     case Link:
36         return QStringLiteral("Link");
37     case Description:
38         return QStringLiteral("Description");
39     case Status:
40         return QStringLiteral("Status");
41     case KeepFlag:
42         return QStringLiteral("KeepFlag");
43     case Author:
44         return QStringLiteral("Author");
45     }
46     return {};
47 }
48 
stringToSubject(const QString & subjStr)49 Criterion::Subject Criterion::stringToSubject(const QString &subjStr)
50 {
51     if (subjStr == QLatin1String("Title")) {
52         return Title;
53     } else if (subjStr == QLatin1String("Link")) {
54         return Link;
55     } else if (subjStr == QLatin1String("Description")) {
56         return Description;
57     } else if (subjStr == QLatin1String("Status")) {
58         return Status;
59     } else if (subjStr == QLatin1String("KeepFlag")) {
60         return KeepFlag;
61     } else if (subjStr == QLatin1String("Author")) {
62         return Author;
63     }
64 
65     // hopefully never reached
66     return Description;
67 }
68 
predicateToString(Predicate pred)69 QString Criterion::predicateToString(Predicate pred)
70 {
71     switch (pred) {
72     case Contains:
73         return QStringLiteral("Contains");
74     case Equals:
75         return QStringLiteral("Equals");
76     case Matches:
77         return QStringLiteral("Matches");
78     case Negation:
79         return QStringLiteral("Negation");
80     }
81     return {};
82 }
83 
stringToPredicate(const QString & predStr)84 Criterion::Predicate Criterion::stringToPredicate(const QString &predStr)
85 {
86     if (predStr == QLatin1String("Contains")) {
87         return Contains;
88     } else if (predStr == QLatin1String("Equals")) {
89         return Equals;
90     } else if (predStr == QLatin1String("Matches")) {
91         return Matches;
92     } else if (predStr == QLatin1String("Negation")) {
93         return Negation;
94     }
95 
96     // hopefully never reached
97     return Contains;
98 }
99 
Criterion()100 Criterion::Criterion()
101 {
102 }
103 
Criterion(Subject subject,Predicate predicate,const QVariant & object)104 Criterion::Criterion(Subject subject, Predicate predicate, const QVariant &object)
105     : m_subject(subject)
106     , m_predicate(predicate)
107     , m_object(object)
108 {
109 }
110 
writeConfig(KConfigGroup * config) const111 void Criterion::writeConfig(KConfigGroup *config) const
112 {
113     config->writeEntry(QStringLiteral("subject"), subjectToString(m_subject));
114 
115     config->writeEntry(QStringLiteral("predicate"), predicateToString(m_predicate));
116 
117     config->writeEntry(QStringLiteral("objectType"), QString::fromLatin1(m_object.typeName()));
118 
119     config->writeEntry(QStringLiteral("objectValue"), m_object);
120 }
121 
readConfig(KConfigGroup * config)122 void Criterion::readConfig(KConfigGroup *config)
123 {
124     m_subject = stringToSubject(config->readEntry(QStringLiteral("subject"), QString()));
125     m_predicate = stringToPredicate(config->readEntry(QStringLiteral("predicate"), QString()));
126     QVariant::Type type = QVariant::nameToType(config->readEntry(QStringLiteral("objType"), QString()).toLatin1().constData());
127 
128     if (type != QVariant::Invalid) {
129         m_object = config->readEntry(QStringLiteral("objectValue"), QVariant(type));
130     }
131 }
132 
satisfiedBy(const Article & article) const133 bool Criterion::satisfiedBy(const Article &article) const
134 {
135     if (article.isNull()) {
136         return false;
137     }
138 
139     QVariant concreteSubject;
140 
141     switch (m_subject) {
142     case Title:
143         concreteSubject = QVariant(article.title());
144         break;
145     case Description:
146         concreteSubject = QVariant(article.description());
147         break;
148     case Link:
149         // ### Maybe use prettyUrl here?
150         concreteSubject = QVariant(article.link().url());
151         break;
152     case Status:
153         concreteSubject = QVariant(article.status());
154         break;
155     case KeepFlag:
156         concreteSubject = QVariant(article.keep());
157         break;
158     case Author:
159         concreteSubject = QVariant(article.authorName());
160     }
161 
162     bool satisfied = false;
163 
164     const auto predicateType = static_cast<Predicate>(m_predicate & ~Negation);
165     QString subjectType = QLatin1String(concreteSubject.typeName());
166 
167     switch (predicateType) {
168     case Contains:
169         satisfied = concreteSubject.toString().indexOf(m_object.toString(), 0, Qt::CaseInsensitive) != -1;
170         break;
171     case Equals:
172         if (subjectType == QLatin1String("int")) {
173             satisfied = concreteSubject.toInt() == m_object.toInt();
174         } else {
175             satisfied = concreteSubject.toString() == m_object.toString();
176         }
177         break;
178     case Matches:
179         satisfied = concreteSubject.toString().contains(QRegularExpression(m_object.toString()));
180         break;
181     default:
182         qCDebug(AKREGATOR_LOG) << "Internal inconsistency; predicateType should never be Negation";
183         break;
184     }
185 
186     if (m_predicate & Negation) {
187         satisfied = !satisfied;
188     }
189 
190     return satisfied;
191 }
192 
subject() const193 Criterion::Subject Criterion::subject() const
194 {
195     return m_subject;
196 }
197 
predicate() const198 Criterion::Predicate Criterion::predicate() const
199 {
200     return m_predicate;
201 }
202 
object() const203 QVariant Criterion::object() const
204 {
205     return m_object;
206 }
207 
ArticleMatcher()208 ArticleMatcher::ArticleMatcher()
209     : m_association(None)
210 {
211 }
212 
~ArticleMatcher()213 ArticleMatcher::~ArticleMatcher()
214 {
215 }
216 
ArticleMatcher(const QVector<Criterion> & criteria,Association assoc)217 ArticleMatcher::ArticleMatcher(const QVector<Criterion> &criteria, Association assoc)
218     : m_criteria(criteria)
219     , m_association(assoc)
220 {
221 }
222 
matches(const Article & a) const223 bool ArticleMatcher::matches(const Article &a) const
224 {
225     switch (m_association) {
226     case LogicalOr:
227         return anyCriterionMatches(a);
228     case LogicalAnd:
229         return allCriteriaMatch(a);
230     default:
231         break;
232     }
233     return true;
234 }
235 
writeConfig(KConfigGroup * config) const236 void ArticleMatcher::writeConfig(KConfigGroup *config) const
237 {
238     config->writeEntry(QStringLiteral("matcherAssociation"), associationToString(m_association));
239 
240     config->writeEntry(QStringLiteral("matcherCriteriaCount"), m_criteria.count());
241 
242     QString criterionGroupPrefix = config->name() + QLatin1String("_Criterion");
243 
244     const int criteriaSize(m_criteria.size());
245     for (int index = 0; index < criteriaSize; ++index) {
246         *config = KConfigGroup(config->config(), criterionGroupPrefix + QString::number(index));
247         m_criteria.at(index).writeConfig(config);
248     }
249 }
250 
readConfig(KConfigGroup * config)251 void ArticleMatcher::readConfig(KConfigGroup *config)
252 {
253     m_criteria.clear();
254     m_association = stringToAssociation(config->readEntry(QStringLiteral("matcherAssociation"), QString()));
255 
256     const int count = config->readEntry(QStringLiteral("matcherCriteriaCount"), 0);
257 
258     const QString criterionGroupPrefix = config->name() + QLatin1String("_Criterion");
259 
260     for (int i = 0; i < count; ++i) {
261         Criterion c;
262         *config = KConfigGroup(config->config(), criterionGroupPrefix + QString::number(i));
263         c.readConfig(config);
264         m_criteria.append(c);
265     }
266 }
267 
operator ==(const AbstractMatcher & other) const268 bool ArticleMatcher::operator==(const AbstractMatcher &other) const
269 {
270     auto ptr = const_cast<AbstractMatcher *>(&other);
271     auto o = dynamic_cast<ArticleMatcher *>(ptr);
272     if (!o) {
273         return false;
274     } else {
275         return m_association == o->m_association && m_criteria == o->m_criteria;
276     }
277 }
278 
operator !=(const AbstractMatcher & other) const279 bool ArticleMatcher::operator!=(const AbstractMatcher &other) const
280 {
281     return !(*this == other);
282 }
283 
anyCriterionMatches(const Article & a) const284 bool ArticleMatcher::anyCriterionMatches(const Article &a) const
285 {
286     if (m_criteria.isEmpty()) {
287         return true;
288     }
289     const int criteriaSize(m_criteria.size());
290     for (int index = 0; index < criteriaSize; ++index) {
291         if (m_criteria.at(index).satisfiedBy(a)) {
292             return true;
293         }
294     }
295     return false;
296 }
297 
allCriteriaMatch(const Article & a) const298 bool ArticleMatcher::allCriteriaMatch(const Article &a) const
299 {
300     if (m_criteria.isEmpty()) {
301         return true;
302     }
303     const int criteriaSize(m_criteria.size());
304     for (int index = 0; index < criteriaSize; ++index) {
305         if (!m_criteria.at(index).satisfiedBy(a)) {
306             return false;
307         }
308     }
309     return true;
310 }
311 
stringToAssociation(const QString & assocStr)312 ArticleMatcher::Association ArticleMatcher::stringToAssociation(const QString &assocStr)
313 {
314     if (assocStr == QLatin1String("LogicalAnd")) {
315         return LogicalAnd;
316     } else if (assocStr == QLatin1String("LogicalOr")) {
317         return LogicalOr;
318     } else {
319         return None;
320     }
321 }
322 
associationToString(Association association)323 QString ArticleMatcher::associationToString(Association association)
324 {
325     switch (association) {
326     case LogicalAnd:
327         return QStringLiteral("LogicalAnd");
328     case LogicalOr:
329         return QStringLiteral("LogicalOr");
330     default:
331         return QStringLiteral("None");
332     }
333 }
334 } // namespace Filters
335 } // namespace Akregator
336