Akonadi Contacts

standardcontactformatter.cpp
1 /*
2  This file is part of Akonadi Contact.
3 
4  SPDX-FileCopyrightText: 2010 Tobias Koenig <[email protected]>
5 
6  SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include "standardcontactformatter.h"
10 
11 #include <Akonadi/Item>
12 #include <KColorScheme>
13 #include <KConfigGroup>
14 
15 #include <KContacts/Addressee>
16 #include <kcontacts_version.h>
17 
18 #include <KLocalizedString>
19 #include <KStringHandler>
20 
21 #include <QLocale>
22 #include <QRegularExpression>
23 #include <QSet>
24 
25 using namespace Akonadi;
26 
27 class Akonadi::StandardContactFormatterPrivate
28 {
29 public:
30  bool displayQRcode = true;
31 };
32 
34  : d(new StandardContactFormatterPrivate())
35 {
36 }
37 
39 
40 static int contactAge(const QDate &date)
41 {
42  const QDate now = QDate::currentDate();
43  int age = now.year() - date.year();
44  if (date > now.addYears(-age)) {
45  age--;
46  }
47  return age;
48 }
49 
51 {
52  KContacts::Addressee rawContact;
53  const Akonadi::Item localItem = item();
54  if (localItem.isValid() && localItem.hasPayload<KContacts::Addressee>()) {
55  rawContact = localItem.payload<KContacts::Addressee>();
56  } else {
57  rawContact = contact();
58  }
59 
60  if (rawContact.isEmpty()) {
61  return {};
62  }
63 
64  // We'll be building a table to display the vCard in.
65  // Each row of the table will be built using one of these strings for its HTML.
66 
67  // single data item:
68  // %1 is the item name
69  // %2 is the item value
70  QString rowFmtStr1 = QStringLiteral(
71  "<tr valign=\"top\">"
72  "<td align=\"right\" valign=\"top\" width=\"30%\"><b><font color=\"grey\">%1</font></b></td>\n"
73  "<td colspan=\"2\" align=\"left\" valign=\"top\" width=\"70%\"><font>%2</font></td>\n"
74  "</tr>\n");
75 
76  // data item plus additional icon(s):
77  // %1 is the item name
78  // %2 is the item value
79  // %3 is the icon(s), each as a HTML <a><img> tag
80  QString rowFmtStr2 = QStringLiteral(
81  "<tr valign=\"top\">"
82  "<td align=\"right\" valign=\"top\" width=\"30%\"><b><font color=\"grey\">%1</font></b></td>\n"
83  "<td align=\"left\" valign=\"top\"><font>%2</font></td>\n"
84  "<td align=\"left\" valign=\"top\">%3</td>\n"
85  "</tr>\n");
86 
87  // Build the table's rows here
88  QString dynamicPart;
89 
90  // Birthday
91  const QDate date = rawContact.birthday().date();
92  const int years = contactAge(date);
93 
94  if (date.isValid()) {
95  dynamicPart += rowFmtStr1.arg(KContacts::Addressee::birthdayLabel(),
96  QLocale().toString(date) + QLatin1String("&nbsp;&nbsp;") + i18np("(One year old)", "(%1 years old)", years));
97  }
98 
99  // Phone Numbers
100  const auto phoneNumbers = rawContact.phoneNumbers();
101  for (const KContacts::PhoneNumber &number : phoneNumbers) {
102  QString dispLabel = number.typeLabel().replace(QLatin1Char(' '), QStringLiteral("&nbsp;"));
103  QString dispValue = QStringLiteral("<a href=\"tel:%1\">%2</a>").arg(number.normalizedNumber()).arg(number.number().toHtmlEscaped());
104  if (number.type() & KContacts::PhoneNumber::Cell) {
105  QString dispIcon = QStringLiteral("<a href=\"sms:%1\" title=\"%2\"><img src=\"sms_icon\" align=\"top\"/>")
106  .arg(number.normalizedNumber())
107  .arg(i18nc("@info:tooltip", "Send SMS"));
108  dynamicPart += rowFmtStr2.arg(dispLabel, dispValue, dispIcon);
109  } else {
110  dynamicPart += rowFmtStr1.arg(dispLabel, dispValue);
111  }
112  }
113 
114  // EMails
115  for (const QString &email : rawContact.emails()) {
116  const QString type = i18nc("a contact's email address", "Email");
117 
118  const QString fullEmail = QString::fromLatin1(QUrl::toPercentEncoding(rawContact.fullEmail(email)));
119 
120  dynamicPart += rowFmtStr1.arg(type, QStringLiteral("<a href=\"mailto:%1\">%2</a>").arg(fullEmail, email));
121  }
122 
123  // Homepage
124  if (rawContact.url().isValid()) {
125  QString url = rawContact.url().url().url();
126  if (!url.startsWith(QLatin1String("http://")) && !url.startsWith(QLatin1String("https://"))) {
127  url = QLatin1String("http://") + url;
128  }
129 
131  dynamicPart += rowFmtStr1.arg(i18n("Homepage"), url);
132  }
133 
134  // Blog Feed
135  const QString blog = rawContact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("BlogFeed"));
136  if (!blog.isEmpty()) {
137  dynamicPart += rowFmtStr1.arg(i18n("Blog Feed"), KStringHandler::tagUrls(blog.toHtmlEscaped()));
138  }
139 
140  // Addresses
141  int counter = 0;
142  const auto addresses = rawContact.addresses();
143  for (const KContacts::Address &address : addresses) {
144  QString formattedAddress;
145 
146  if (address.label().isEmpty()) {
147  formattedAddress = address.formatted(KContacts::AddressFormatStyle::Postal).trimmed().toHtmlEscaped();
148  } else {
149  formattedAddress = address.label().toHtmlEscaped();
150  }
151 
152  formattedAddress.replace(QRegularExpression(QStringLiteral("\n+")), QStringLiteral("<br>"));
153 
154 #if KContacts_VERSION < QT_VERSION_CHECK(5, 106, 0)
155  const QString url = QStringLiteral("<a href=\"address:?index=%1\" title=\"%2\"><img src=\"map_icon\" alt=\"%2\"/></a>")
156  .arg(counter)
157  .arg(i18nc("@info:tooltip", "Show address on map"));
158  counter++;
159 #else
160  const QString url = QStringLiteral("<a href=\"%1\" title=\"%2\"><img src=\"map_icon\" alt=\"%2\"/></a>")
161  .arg(address.geoUri().toString())
162  .arg(i18nc("@info:tooltip", "Show address on map"));
163 #endif
164 
165  dynamicPart += rowFmtStr2.arg(KContacts::Address::typeLabel(address.type()), formattedAddress, url);
166  }
167 
168  // Note
169  QString notes;
170  if (!rawContact.note().isEmpty()) {
171  notes = rowFmtStr1.arg(i18n("Notes"), rawContact.note().toHtmlEscaped().replace(QLatin1Char('\n'), QLatin1String("<br>")));
172  }
173 
174  // Custom Data
175  QString customData;
176  static QMap<QString, QString> titleMap;
177  if (titleMap.isEmpty()) {
178  titleMap.insert(QStringLiteral("Department"), i18n("Department"));
179  titleMap.insert(QStringLiteral("Profession"), i18n("Profession"));
180  titleMap.insert(QStringLiteral("AssistantsName"), i18n("Assistant's Name"));
181  titleMap.insert(QStringLiteral("ManagersName"), i18n("Manager's Name"));
182  titleMap.insert(QStringLiteral("SpousesName"), i18nc("Wife/Husband/...", "Partner's Name"));
183  titleMap.insert(QStringLiteral("Office"), i18n("Office"));
184  titleMap.insert(QStringLiteral("IMAddress"), i18n("IM Address"));
185  titleMap.insert(QStringLiteral("Anniversary"), i18n("Anniversary"));
186  titleMap.insert(QStringLiteral("AddressBook"), i18n("Address Book"));
187  const QMap<QString, QString> tmpTitleMap = titleMap;
188 
189  QMap<QString, QString>::const_iterator iterator = tmpTitleMap.constBegin();
190  while (iterator != tmpTitleMap.constEnd()) {
191  titleMap.insert(iterator.key().toUpper(), iterator.value());
192  ++iterator;
193  }
194  }
195 
196  static QSet<QString> blacklistedKeys;
197  if (blacklistedKeys.isEmpty()) {
198  blacklistedKeys.insert(QStringLiteral("CRYPTOPROTOPREF"));
199  blacklistedKeys.insert(QStringLiteral("OPENPGPFP"));
200  blacklistedKeys.insert(QStringLiteral("SMIMEFP"));
201  blacklistedKeys.insert(QStringLiteral("CRYPTOSIGNPREF"));
202  blacklistedKeys.insert(QStringLiteral("CRYPTOENCRYPTPREF"));
203  blacklistedKeys.insert(QStringLiteral("MailPreferedFormatting"));
204  blacklistedKeys.insert(QStringLiteral("MailAllowToRemoteContent"));
205  blacklistedKeys.insert(QStringLiteral("MAILPREFEREDFORMATTING"));
206  blacklistedKeys.insert(QStringLiteral("MAILALLOWTOREMOTECONTENT"));
207  }
208 
209  if (!rawContact.customs().empty()) {
210  const QStringList customs = rawContact.customs();
211  for (QString custom : customs) {
212  if (custom.startsWith(QLatin1String("KADDRESSBOOK-"))) {
213  custom.remove(QStringLiteral("KADDRESSBOOK-X-"));
214  custom.remove(QStringLiteral("KADDRESSBOOK-"));
215 
216  int pos = custom.indexOf(QLatin1Char(':'));
217  QString key = custom.left(pos);
218  QString value = custom.mid(pos + 1);
219 
220  // convert anniversary correctly
221  if (key == QLatin1String("Anniversary") || key == QLatin1String("ANNIVERSARY")) {
222  const QDateTime dateTime = QDateTime::fromString(value, Qt::ISODate);
223  value = QLocale().toString(dateTime.date());
224  } else if (key == QLatin1String("BlogFeed") || key == QLatin1String("BLOGFEED")) { // blog is handled separated
225  continue;
226  } else if (blacklistedKeys.contains(key)) {
227  continue;
228  }
229 
230  // check whether we have a mapping for the title
231  const QMap<QString, QString>::ConstIterator keyIt = titleMap.constFind(key);
232  bool needToEscape = true;
233  if (keyIt != titleMap.constEnd()) {
234  key = keyIt.value();
235  } else {
236  // check whether it is a custom local field
237  for (const QVariantMap &description : customFieldDescriptions()) {
238  if (description.value(QStringLiteral("key")).toString() == key) {
239  key = description.value(QStringLiteral("title")).toString();
240  const QString descriptionType = description.value(QStringLiteral("type")).toString();
241  if (descriptionType == QLatin1String("boolean")) {
242  if (value == QLatin1String("true")) {
243  value = i18nc("Boolean value", "yes");
244  } else {
245  value = i18nc("Boolean value", "no");
246  }
247  } else if (descriptionType == QLatin1String("date")) {
248  const QDate date = QDate::fromString(value, Qt::ISODate);
249  value = QLocale().toString(date, QLocale::ShortFormat);
250  } else if (descriptionType == QLatin1String("time")) {
251  const QTime time = QTime::fromString(value, Qt::ISODate);
252  value = QLocale().toString(time);
253  } else if (descriptionType == QLatin1String("datetime")) {
254  const QDateTime dateTime = QDateTime::fromString(value, Qt::ISODate);
255  value = QLocale().toString(dateTime, QLocale::ShortFormat);
256  } else if (descriptionType == QLatin1String("url")) {
257  value = KStringHandler::tagUrls(value.toHtmlEscaped());
258  needToEscape = false;
259  }
260 
261  break;
262  }
263  }
264  }
265  if (needToEscape) {
266  value = value.toHtmlEscaped();
267  }
268  customData += rowFmtStr1.arg(key, value);
269  }
270  }
271  }
272 
273  // Assemble all parts
274  QString role = rawContact.title();
275  if (role.isEmpty()) {
276  role = rawContact.role();
277  }
278  if (role.isEmpty()) {
279  role = rawContact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Profession"));
280  }
281 
282  QString strAddr =
283  QStringLiteral(
284  "<div align=\"center\">"
285  "<table cellpadding=\"3\" cellspacing=\"1\">"
286  "<tr>"
287  "<td align=\"right\" valign=\"top\" width=\"30%\" rowspan=\"3\">"
288  "<img src=\"%1\" width=\"100\" vspace=\"1\"/>" // image
289  "</td>"
290  "<td colspan=\"2\" align=\"left\" width=\"70%\"><font size=\"+2\"><b>%2</b></font></td>" // name
291  "</tr>"
292  "<tr>"
293  "<td colspan=\"2\" align=\"left\" width=\"70%\">%3</td>" // role
294  "</tr>"
295  "<tr>"
296  "<td colspan=\"2\" align=\"left\" width=\"70%\">%4</td>" // organization
297  "</tr>")
298  .arg(QStringLiteral("contact_photo"), rawContact.realName().toHtmlEscaped(), role.toHtmlEscaped(), rawContact.organization().toHtmlEscaped());
299 
300  strAddr.append(dynamicPart);
301  strAddr.append(notes);
302  strAddr.append(customData);
303  strAddr.append(QLatin1String("</table>"));
304 
305  if (d->displayQRcode) {
306  KConfig config(QStringLiteral("akonadi_contactrc"));
307  KConfigGroup group(&config, QStringLiteral("View"));
308  if (group.readEntry("QRCodes", true)) {
309  strAddr.append(QString::fromLatin1("<p align=\"center\">"
310  "<img src=\"%1\" vspace=\"1\"/>"
311  "</p>")
312  .arg(QStringLiteral("qrcode")));
313  }
314  }
315 
316  strAddr.append(QLatin1String("</div>\n"));
317 
318  if (form == EmbeddableForm) {
319  return strAddr;
320  }
321 
322  const QString document = QStringLiteral(
323  "<html>"
324  "<head>"
325  " <style type=\"text/css\">"
326  " a {text-decoration:none; color:%1}"
327  " </style>"
328  "</head>"
329  "<body text=\"%1\" bgcolor=\"%2\">" // text and background color
330  "%3" // contact part
331  "</body>"
332  "</html>")
333  .arg(KColorScheme(QPalette::Active, KColorScheme::View).foreground().color().name(),
334  KColorScheme(QPalette::Active, KColorScheme::View).background().color().name(),
335  strAddr);
336 
337  return document;
338 }
339 
340 void StandardContactFormatter::setDisplayQRCode(bool show)
341 {
342  d->displayQRcode = show;
343 }
344 
345 bool StandardContactFormatter::displayQRCode() const
346 {
347  return d->displayQRcode;
348 }
bool isValid() const
QMap::const_iterator constBegin() const const
QString title() const
QString typeLabel() const
HtmlForm
Describes the form of the HTML that is created.
QString readEntry(const char *key, const char *aDefault=nullptr) const
QString number(int n, int base)
QTime fromString(const QString &string, Qt::DateFormat format)
QString toHtmlEscaped() const const
QString custom(const QString &app, const QString &name) const
QString organization() const
const T value(const Key &key, const T &defaultValue) const const
int year() const const
QString realName() const
KContacts::Addressee contact() const
Returns the contact that will be formatted.
QString note() const
PhoneNumber::List phoneNumbers() const
QMap::const_iterator constFind(const Key &key) const const
bool hasPayload() const
bool isEmpty() const
QMap::iterator insert(const Key &key, const T &value)
bool empty() const const
QString i18n(const char *text, const TYPE &arg...)
ResourceLocatorUrl url() const
QDateTime birthday() const
QMap::const_iterator constEnd() const const
static QString birthdayLabel()
bool isEmpty() const const
QString role() const
QString toString(qlonglong i) const const
bool isValid() const const
QVector< QVariantMap > customFieldDescriptions() const
Returns the custom field descriptions that will be used.
KCOREADDONS_EXPORT QString tagUrls(const QString &text)
QDate currentDate()
QDate addYears(int nyears) const const
QDateTime fromString(const QString &string, Qt::DateFormat format)
QByteArray toPercentEncoding(const QString &input, const QByteArray &exclude, const QByteArray &include)
int indexOf(QStringView str, int from) const const
~StandardContactFormatter() override
Destroys the standard contact formatter.
const Key key(const T &value, const Key &defaultKey) const const
bool contains(const T &value) const const
QString & replace(int position, int n, QChar after)
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
Akonadi::Item item() const
Returns the item who's payload will be formatted.
StandardContactFormatter()
Creates a new standard contact formatter.
QDate fromString(const QString &string, Qt::DateFormat format)
QString fullEmail(const QString &email=QString()) const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString left(int n) const const
@ EmbeddableForm
Creates a div HTML element that can be embedded.
QString fromLatin1(const char *str, int size)
QDate date() const const
QString toHtml(HtmlForm form=SelfcontainedForm) const override
Returns the contact formatted as HTML.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QSet::iterator insert(const T &value)
Address::List addresses() const
QStringList customs() const
T payload() const
QStringList emails() const
QString mid(int position, int n) const const
QString & append(QChar ch)
bool isEmpty() const const
bool isEmpty() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sat Apr 1 2023 04:09:05 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.