9#include "notification.h"
10#include "notification_p.h"
14#include <QDBusArgument>
16#include <QImageReader>
17#include <QRegularExpression>
18#include <QXmlStreamReader>
20#include <KApplicationTrader>
22#include <KConfigGroup>
27using namespace NotificationManager;
28using namespace Qt::StringLiterals;
32Notification::Private::Private()
36Notification::Private::~Private()
57 static const QRegularExpression escapeExpr(QStringLiteral(
"&(?!(?:apos|quot|[gl]t|amp);|#)"));
65 t = u
"<html>" + std::move(t) + u
"</html>";
70 static constexpr std::array<QStringView, 10> allowedTags{u
"b", u
"i", u
"u", u
"img", u
"a", u
"html", u
"br", u
"table", u
"tr", u
"td"};
72 out.writeStartDocument();
78 if (std::ranges::find(allowedTags, name) == allowedTags.end()) {
81 out.writeStartElement(name);
83 const QString src = r.attributes().value(
"src").toString();
84 const QStringView alt = r.attributes().value(
"alt");
87 if (url.isLocalFile()) {
88 out.writeAttribute(QStringLiteral(
"src"), src);
93 out.writeAttribute(u
"alt", alt);
96 out.writeAttribute(u
"href", r.attributes().value(
"href"));
102 if (std::ranges::find(allowedTags, name) == allowedTags.end()) {
105 out.writeEndElement();
109 out.writeCharacters(r.text());
112 out.writeEndDocument();
115 qCWarning(NOTIFICATIONMANAGER) <<
"Notification to send to backend contains invalid XML: " << r.errorString() <<
"line" << r.lineNumber() <<
"col"
128 int width, height, rowStride, hasAlpha, bitsPerSample, channels;
137 arg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> pixels;
140#define SANITY_CHECK(condition) \
141 if (!(condition)) { \
142 qCWarning(NOTIFICATIONMANAGER) << "Image decoding sanity check failed on" << #condition; \
146 SANITY_CHECK(width > 0);
147 SANITY_CHECK(width < 2048);
148 SANITY_CHECK(height > 0);
149 SANITY_CHECK(height < 2048);
150 SANITY_CHECK(rowStride > 0);
154 auto copyLineRGB32 = [](QRgb *dst,
const char *src,
int width) {
155 const char *
end = src + width * 3;
156 for (; src !=
end; ++dst, src += 3) {
157 *dst = qRgb(src[0], src[1], src[2]);
161 auto copyLineARGB32 = [](QRgb *dst,
const char *src,
int width) {
162 const char *
end = src + width * 4;
163 for (; src !=
end; ++dst, src += 4) {
164 *dst = qRgba(src[0], src[1], src[2], src[3]);
169 void (*fcn)(QRgb *,
const char *, int) =
nullptr;
170 if (bitsPerSample == 8) {
173 fcn = copyLineARGB32;
174 }
else if (channels == 3) {
180 qCWarning(NOTIFICATIONMANAGER) <<
"Unsupported image format (hasAlpha:" << hasAlpha <<
"bitsPerSample:" << bitsPerSample <<
"channels:" << channels
185 QImage image(width, height, format);
187 end = ptr + pixels.length();
188 for (
int y = 0; y < height; ++y, ptr += rowStride) {
189 if (ptr + channels * width > end) {
190 qCWarning(NOTIFICATIONMANAGER) <<
"Image data is incomplete. y:" << y <<
"height:" << height;
193 fcn((QRgb *)image.scanLine(y), ptr, width);
199void Notification::Private::sanitizeImage(
QImage &image)
205 const QSize max = maximumImageSize();
211void Notification::Private::loadImagePath(
const QString &path)
216 s_imageCache.remove(
id);
223 imageUrl =
QUrl(path);
226 qCDebug(NOTIFICATIONMANAGER) <<
"Refused to load image from" <<
path <<
"which isn't a valid local location.";
238 reader.setAutoTransform(
true);
240 if (
QSize imageSize = reader.size(); imageSize.
isValid()) {
241 if (imageSize.width() > maximumImageSize().width() || imageSize.height() > maximumImageSize().height()) {
243 reader.setScaledSize(imageSize);
245 s_imageCache.insert(
id,
new QImage(reader.read()), imageSize.
width() * imageSize.height());
249QString Notification::Private::defaultComponentName()
252 return QStringLiteral(
"plasma_workspace");
255constexpr QSize Notification::Private::maximumImageSize()
257 return QSize(256, 256);
285 return renamedFrom.
contains(desktopId);
288 if (!services.isEmpty()) {
289 service = services.first();
296 const QString snapInstanceName = app->property<
QString>(QStringLiteral(
"X-SnapInstanceName"));
300 if (!services.isEmpty()) {
301 service = services.first();
308void Notification::Private::setDesktopEntry(
const QString &desktopEntry)
312 configurableService =
false;
314 KService::Ptr service = serviceForDesktopEntry(desktopEntry);
316 this->desktopEntry = service->desktopEntryName();
317 serviceName = service->name();
318 applicationIconName = service->icon();
319 configurableService = !service->noDisplay();
322 const bool isDefaultEvent = (notifyRcName == defaultComponentName());
323 configurableNotifyRc =
false;
324 if (!notifyRcName.isEmpty()) {
336 std::reverse(configSources.
begin(), configSources.
end());
337 config.addConfigSources(configSources);
341 const QString iconName = globalGroup.readEntry(
"IconName");
344 if (!iconName.
isEmpty() && (!isDefaultEvent || applicationIconName.isEmpty())) {
345 applicationIconName = iconName;
349 configurableNotifyRc = !config.groupList().filter(regexp).isEmpty();
355 if ((isDefaultEvent || applicationName.isEmpty()) && !serviceName.
isEmpty()) {
356 applicationName = serviceName;
360void Notification::Private::processHints(
const QVariantMap &hints)
362 auto end = hints.end();
364 notifyRcName = hints.value(QStringLiteral(
"x-kde-appname")).toString();
366 setDesktopEntry(hints.value(QStringLiteral(
"desktop-entry")).
toString());
370 const QString applicationDisplayName = hints.value(QStringLiteral(
"x-kde-display-appname")).toString();
371 if (!applicationDisplayName.
isEmpty()) {
372 applicationName = applicationDisplayName;
375 originName = hints.value(QStringLiteral(
"x-kde-origin-name")).toString();
377 eventId = hints.value(QStringLiteral(
"x-kde-eventId")).toString();
378 xdgTokenAppId = hints.value(QStringLiteral(
"x-kde-xdgTokenAppId")).toString();
381 const int urgency = hints.value(QStringLiteral(
"urgency")).toInt(&ok);
392 setUrgency(Notifications::CriticalUrgency);
397 resident = hints.value(QStringLiteral(
"resident")).toBool();
398 transient = hints.value(QStringLiteral(
"transient")).toBool();
400 userActionFeedback = hints.value(QStringLiteral(
"x-kde-user-action-feedback")).toBool();
401 if (userActionFeedback) {
408 replyPlaceholderText = hints.value(QStringLiteral(
"x-kde-reply-placeholder-text")).toString();
409 replySubmitButtonText = hints.value(QStringLiteral(
"x-kde-reply-submit-button-text")).toString();
410 replySubmitButtonIconName = hints.value(QStringLiteral(
"x-kde-reply-submit-button-icon-name")).toString();
412 category = hints.value(QStringLiteral(
"category")).toString();
417 auto it = hints.find(QStringLiteral(
"image-data"));
419 it = hints.find(QStringLiteral(
"image_data"));
425 it = hints.find(QStringLiteral(
"icon_data"));
431 if (!imageFromHint.isNull()) {
432 Q_ASSERT_X(imageFromHint.width() > 0 && imageFromHint.height() > 0,
435 sanitizeImage(imageFromHint);
436 image =
new QImage(imageFromHint);
437 s_imageCache.insert(
id, image, imageFromHint.width() * imageFromHint.height());
442 it = hints.find(QStringLiteral(
"image-path"));
444 it = hints.find(QStringLiteral(
"image_path"));
448 loadImagePath(it->toString());
455 this->urgency = urgency;
461 if (urgency == Notifications::CriticalUrgency) {
466Notification::Notification(uint
id)
474 : d(new Private(*other.d))
478Notification::Notification(
Notification &&other) noexcept
497Notification::~Notification()
502uint Notification::id()
const
507QString Notification::dBusService()
const
509 return d->dBusService;
512void Notification::setDBusService(
const QString &dBusService)
514 d->dBusService = dBusService;
522void Notification::setCreated(
const QDateTime &created)
524 d->created = created;
532void Notification::resetUpdated()
537bool Notification::read()
const
542void Notification::setRead(
bool read)
547QString Notification::summary()
const
552void Notification::setSummary(
const QString &summary)
554 d->summary = summary;
557QString Notification::body()
const
562void Notification::setBody(
const QString &body)
565 d->body = Private::sanitize(body.
trimmed());
568QString Notification::rawBody()
const
573QString Notification::icon()
const
578void Notification::setIcon(
const QString &icon)
580 d->loadImagePath(icon);
583QImage Notification::image()
const
585 if (d->s_imageCache.contains(d->id)) {
586 return *d->s_imageCache.object(d->id);
591void Notification::setImage(
const QImage &image)
593 d->s_imageCache.insert(d->id,
new QImage(image));
596QString Notification::desktopEntry()
const
598 return d->desktopEntry;
601void Notification::setDesktopEntry(
const QString &desktopEntry)
603 d->setDesktopEntry(desktopEntry);
606QString Notification::notifyRcName()
const
608 return d->notifyRcName;
611QString Notification::eventId()
const
616QString Notification::applicationName()
const
618 return d->applicationName;
621void Notification::setApplicationName(
const QString &applicationName)
623 d->applicationName = applicationName;
626QString Notification::applicationIconName()
const
628 return d->applicationIconName;
631void Notification::setApplicationIconName(
const QString &applicationIconName)
633 d->applicationIconName = applicationIconName;
636QString Notification::originName()
const
638 return d->originName;
643 return d->actionNames;
648 return d->actionLabels;
651bool Notification::hasDefaultAction()
const
653 return d->hasDefaultAction;
656QString Notification::defaultActionLabel()
const
658 return d->defaultActionLabel;
661void Notification::setActions(
const QStringList &actions)
663 if (actions.
count() % 2 != 0) {
664 qCWarning(NOTIFICATIONMANAGER) <<
"List of actions must contain an even number of items, tried to set actions to" << actions;
668 d->hasDefaultAction =
false;
669 d->hasConfigureAction =
false;
670 d->hasReplyAction =
false;
675 for (
int i = 0; i < actions.
count(); i += 2) {
679 if (!d->hasDefaultAction && name ==
QLatin1String(
"default")) {
680 d->hasDefaultAction =
true;
681 d->defaultActionLabel =
label;
685 if (!d->hasConfigureAction && name ==
QLatin1String(
"settings")) {
686 d->hasConfigureAction =
true;
687 d->configureActionLabel =
label;
691 if (!d->hasReplyAction && name ==
QLatin1String(
"inline-reply")) {
692 d->hasReplyAction =
true;
693 d->replyActionLabel =
label;
701 d->actionNames = names;
702 d->actionLabels = labels;
720bool Notification::userActionFeedback()
const
722 return d->userActionFeedback;
725int Notification::timeout()
const
730void Notification::setTimeout(
int timeout)
732 d->timeout = timeout;
735bool Notification::configurable()
const
737 return d->hasConfigureAction || d->configurableNotifyRc || d->configurableService;
740QString Notification::configureActionLabel()
const
742 return d->configureActionLabel;
745bool Notification::hasReplyAction()
const
747 return d->hasReplyAction;
750QString Notification::replyActionLabel()
const
752 return d->replyActionLabel;
755QString Notification::replyPlaceholderText()
const
757 return d->replyPlaceholderText;
760QString Notification::replySubmitButtonText()
const
762 return d->replySubmitButtonText;
765QString Notification::replySubmitButtonIconName()
const
767 return d->replySubmitButtonIconName;
770QString Notification::category()
const
775bool Notification::expired()
const
780void Notification::setExpired(
bool expired)
782 d->expired = expired;
785bool Notification::dismissed()
const
790void Notification::setDismissed(
bool dismissed)
792 d->dismissed = dismissed;
795bool Notification::resident()
const
800void Notification::setResident(
bool resident)
802 d->resident = resident;
805bool Notification::transient()
const
810void Notification::setTransient(
bool transient)
815QVariantMap Notification::hints()
const
820void Notification::setHints(
const QVariantMap &hints)
825void Notification::processHints(
const QVariantMap &hints)
827 d->processHints(hints);
static Ptr serviceByDesktopName(const QString &_name)
static Ptr serviceByDesktopPath(const QString &_path)
Represents a single notification.
Urgency
The notification urgency.
@ LowUrgency
The notification has low urgency, it is not important and may not be shown or added to a history.
@ NormalUrgency
The notification has normal urgency. This is also the default if no urgecny is supplied.
char * toString(const EngineQuery &query)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
QVariant read(const QByteArray &data, int versionOverride=0)
QString path(const QString &relativePath)
void transient(const QString &message, const QString &title)
QString name(StandardAction id)
Category category(StandardShortcut id)
QString label(StandardShortcut id)
const QList< QKeySequence > & end()
QDateTime currentDateTimeUtc()
ElementType currentType() const const
bool isNull() const const
QImage scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
const_reference at(qsizetype i) const const
qsizetype count() const const
bool isEmpty() const const
bool isValid() const const
QStringList locateAll(StandardLocation type, const QString &fileName, LocateOptions options)
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString simplified() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toLower() const const
QString trimmed() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
QUrl fromLocalFile(const QString &localFile)
QList< QUrl > fromStringList(const QStringList &urls, ParsingMode mode)
bool isLocalFile() const const
bool isValid() const const
QString toLocalFile() const const