1 /*
2 This file is part of the KContacts framework.
3 SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7
8 #include "address.h"
9
10 #include "kcontacts_debug.h"
11 #include <KConfig>
12 #include <KCountry>
13 #include <KLocalizedString>
14 #include <krandom.h>
15
16 #include <KConfigGroup>
17
18 #include <QDataStream>
19 #include <QLocale>
20 #include <QSharedData>
21 #include <QStandardPaths>
22
23 using namespace KContacts;
24
25 // template tags for address formatting localization
26 #define KCONTACTS_FMTTAG_realname QStringLiteral("%n")
27 #define KCONTACTS_FMTTAG_REALNAME QStringLiteral("%N")
28 #define KCONTACTS_FMTTAG_company QStringLiteral("%cm")
29 #define KCONTACTS_FMTTAG_COMPANY QStringLiteral("%CM")
30 #define KCONTACTS_FMTTAG_pobox QStringLiteral("%p")
31 #define KCONTACTS_FMTTAG_street QStringLiteral("%s")
32 #define KCONTACTS_FMTTAG_STREET QStringLiteral("%S")
33 #define KCONTACTS_FMTTAG_zipcode QStringLiteral("%z")
34 #define KCONTACTS_FMTTAG_location QStringLiteral("%l")
35 #define KCONTACTS_FMTTAG_LOCATION QStringLiteral("%L")
36 #define KCONTACTS_FMTTAG_region QStringLiteral("%r")
37 #define KCONTACTS_FMTTAG_REGION QStringLiteral("%R")
38 #define KCONTACTS_FMTTAG_newline QStringLiteral("\\n")
39 #define KCONTACTS_FMTTAG_condcomma QStringLiteral("%,")
40 #define KCONTACTS_FMTTAG_condwhite QStringLiteral("%w")
41 #define KCONTACTS_FMTTAG_purgeempty QStringLiteral("%0")
42
43 /**
44 Finds the balanced closing bracket starting from the opening bracket at
45 pos in tsection.
46 @return position of closing bracket, -1 for unbalanced brackets
47 */
findBalancedBracket(const QString & tsection,int pos)48 static int findBalancedBracket(const QString &tsection, int pos)
49 {
50 int balancecounter = 0;
51 for (int i = pos + 1; i < tsection.length(); ++i) {
52 if (QLatin1Char(')') == tsection[i] && 0 == balancecounter) {
53 // found end of brackets
54 return i;
55 } else {
56 if (QLatin1Char('(') == tsection[i]) {
57 // nested brackets
58 balancecounter++;
59 }
60 }
61 }
62 return -1;
63 }
64
65 /**
66 Parses a snippet of an address template
67 @param tsection the template string to be parsed
68 @param result QString reference in which the result will be stored
69 @return true if at least one tag evaluated positively, else false
70 */
71 static bool
parseAddressTemplateSection(const QString & tsection,QString & result,const QString & realName,const QString & orgaName,const KContacts::Address & address)72 parseAddressTemplateSection(const QString &tsection, QString &result, const QString &realName, const QString &orgaName, const KContacts::Address &address)
73 {
74 // This method first parses and substitutes any bracketed sections and
75 // after that replaces any tags with their values. If a bracketed section
76 // or a tag evaluate to zero, they are not just removed but replaced
77 // with a placeholder. This is because in the last step conditionals are
78 // resolved which depend on information about zero-evaluations.
79 result = tsection;
80 int stpos = 0;
81 bool ret = false;
82
83 // first check for brackets that have to be evaluated first
84 int fpos = result.indexOf(KCONTACTS_FMTTAG_purgeempty, stpos);
85 while (-1 != fpos) {
86 int bpos1 = fpos + KCONTACTS_FMTTAG_purgeempty.length();
87 // expect opening bracket and find next balanced closing bracket. If
88 // next char is no opening bracket, continue parsing (no valid tag)
89 if (QLatin1Char('(') == result[bpos1]) {
90 int bpos2 = findBalancedBracket(result, bpos1);
91 if (-1 != bpos2) {
92 // we have balanced brackets, recursively parse:
93 QString rplstr;
94 bool purge = !parseAddressTemplateSection(result.mid(bpos1 + 1, bpos2 - bpos1 - 1), //
95 rplstr,
96 realName,
97 orgaName,
98 address);
99 if (purge) {
100 // purge -> remove all
101 // replace with !_P_!, so conditional tags work later
102 result.replace(fpos, bpos2 - fpos + 1, QStringLiteral("!_P_!"));
103 // leave stpos as it is
104 } else {
105 // no purge -> replace with recursively parsed string
106 result.replace(fpos, bpos2 - fpos + 1, rplstr);
107 ret = true;
108 stpos = fpos + rplstr.length();
109 }
110 } else {
111 // unbalanced brackets: keep on parsing (should not happen
112 // and will result in bad formatting)
113 stpos = bpos1;
114 }
115 }
116 fpos = result.indexOf(KCONTACTS_FMTTAG_purgeempty, stpos);
117 }
118
119 // after sorting out all purge tags, we just search'n'replace the rest,
120 // keeping track of whether at least one tag evaluates to something.
121 // The following macro needs QString for R_FIELD
122 // It substitutes !_P_! for empty fields so conditional tags work later
123 // clang-format off
124 #define REPLTAG(R_TAG, R_FIELD) \
125 if (result.contains(R_TAG)) { \
126 QString rpl = R_FIELD.isEmpty() ? QStringLiteral("!_P_!") : R_FIELD; \
127 result.replace(R_TAG, rpl); \
128 if (!R_FIELD.isEmpty()) { \
129 ret = true; \
130 } \
131 }
132 // clang-format on
133 REPLTAG(KCONTACTS_FMTTAG_realname, realName);
134 REPLTAG(KCONTACTS_FMTTAG_REALNAME, realName.toUpper());
135 REPLTAG(KCONTACTS_FMTTAG_company, orgaName);
136 REPLTAG(KCONTACTS_FMTTAG_COMPANY, orgaName.toUpper());
137 REPLTAG(KCONTACTS_FMTTAG_pobox, address.postOfficeBox());
138 REPLTAG(KCONTACTS_FMTTAG_street, address.street());
139 REPLTAG(KCONTACTS_FMTTAG_STREET, address.street().toUpper());
140 REPLTAG(KCONTACTS_FMTTAG_zipcode, address.postalCode());
141 REPLTAG(KCONTACTS_FMTTAG_location, address.locality());
142 REPLTAG(KCONTACTS_FMTTAG_LOCATION, address.locality().toUpper());
143 REPLTAG(KCONTACTS_FMTTAG_region, address.region());
144 REPLTAG(KCONTACTS_FMTTAG_REGION, address.region().toUpper());
145 result.replace(KCONTACTS_FMTTAG_newline, QLatin1String("\n"));
146 #undef REPLTAG
147
148 // conditional comma
149 fpos = result.indexOf(KCONTACTS_FMTTAG_condcomma, 0);
150 while (-1 != fpos) {
151 const QString str1 = result.mid(fpos - 5, 5);
152 const QString str2 = result.mid(fpos + 2, 5);
153 if (str1 != QLatin1String("!_P_!") && str2 != QLatin1String("!_P_!")) {
154 result.replace(fpos, 2, QStringLiteral(", "));
155 } else {
156 result.remove(fpos, 2);
157 }
158 fpos = result.indexOf(KCONTACTS_FMTTAG_condcomma, fpos);
159 }
160 // conditional whitespace
161 fpos = result.indexOf(KCONTACTS_FMTTAG_condwhite, 0);
162 while (-1 != fpos) {
163 const QString str1 = result.mid(fpos - 5, 5);
164 const QString str2 = result.mid(fpos + 2, 5);
165 if (str1 != QLatin1String("!_P_!") && str2 != QLatin1String("!_P_!")) {
166 result.replace(fpos, 2, QLatin1Char(' '));
167 } else {
168 result.remove(fpos, 2);
169 }
170 fpos = result.indexOf(KCONTACTS_FMTTAG_condwhite, fpos);
171 }
172
173 // remove purged:
174 result.remove(QStringLiteral("!_P_!"));
175
176 return ret;
177 }
178
179 class Q_DECL_HIDDEN Address::Private : public QSharedData
180 {
181 public:
Private()182 Private()
183 : mEmpty(true)
184 {
185 mId = KRandom::randomString(10);
186 }
187
Private(const Private & other)188 Private(const Private &other)
189 : QSharedData(other)
190 {
191 mEmpty = other.mEmpty;
192 mId = other.mId;
193 mType = other.mType;
194
195 mPostOfficeBox = other.mPostOfficeBox;
196 mExtended = other.mExtended;
197 mStreet = other.mStreet;
198 mLocality = other.mLocality;
199 mRegion = other.mRegion;
200 mPostalCode = other.mPostalCode;
201 mCountry = other.mCountry;
202 mLabel = other.mLabel;
203 }
204
205 bool mEmpty;
206 QString mId;
207 Type mType;
208 Geo mGeo;
209
210 QString mPostOfficeBox;
211 QString mExtended;
212 QString mStreet;
213 QString mLocality;
214 QString mRegion;
215 QString mPostalCode;
216 QString mCountry;
217 QString mLabel;
218 };
219
Address()220 Address::Address()
221 : d(new Private)
222 {
223 }
224
Address(Type type)225 Address::Address(Type type)
226 : d(new Private)
227 {
228 d->mType = type;
229 }
230
Address(const Address & other)231 Address::Address(const Address &other)
232 : d(other.d)
233 {
234 }
235
~Address()236 Address::~Address()
237 {
238 }
239
operator =(const Address & other)240 Address &Address::operator=(const Address &other)
241 {
242 if (this != &other) {
243 d = other.d;
244 }
245
246 return *this;
247 }
248
operator ==(const Address & other) const249 bool Address::operator==(const Address &other) const
250 {
251 if (d->mId != other.d->mId) {
252 return false;
253 }
254 if (d->mType != other.d->mType) {
255 return false;
256 }
257 if (d->mPostOfficeBox != other.d->mPostOfficeBox) {
258 return false;
259 }
260 if (d->mExtended != other.d->mExtended) {
261 return false;
262 }
263 if (d->mStreet != other.d->mStreet) {
264 return false;
265 }
266 if (d->mLocality != other.d->mLocality) {
267 return false;
268 }
269 if (d->mRegion != other.d->mRegion) {
270 return false;
271 }
272 if (d->mPostalCode != other.d->mPostalCode) {
273 return false;
274 }
275 if (d->mCountry != other.d->mCountry) {
276 return false;
277 }
278 if (d->mLabel != other.d->mLabel) {
279 return false;
280 }
281
282 if (d->mGeo != other.d->mGeo) {
283 return false;
284 }
285
286 return true;
287 }
288
operator !=(const Address & a) const289 bool Address::operator!=(const Address &a) const
290 {
291 return !(a == *this);
292 }
293
isEmpty() const294 bool Address::isEmpty() const
295 {
296 return d->mEmpty;
297 }
298
clear()299 void Address::clear()
300 {
301 *this = Address();
302 }
303
setId(const QString & id)304 void Address::setId(const QString &id)
305 {
306 d->mEmpty = false;
307 d->mId = id;
308 }
309
id() const310 QString Address::id() const
311 {
312 return d->mId;
313 }
314
setType(Type type)315 void Address::setType(Type type)
316 {
317 d->mEmpty = false;
318 d->mType = type;
319 }
320
type() const321 Address::Type Address::type() const
322 {
323 return d->mType;
324 }
325
typeLabel(Type type)326 QString Address::typeLabel(Type type)
327 {
328 QString label;
329 const TypeList list = typeList();
330
331 for (const auto typeFlag : list) {
332 // these are actually flags
333 const TypeFlag flag = static_cast<TypeFlag>(static_cast<int>(typeFlag));
334 if (type & flag) {
335 label.append(QLatin1Char('/') + typeFlagLabel(flag));
336 }
337 }
338
339 // Remove the first '/'
340 if (!label.isEmpty()) {
341 label.remove(0, 1);
342 }
343
344 return label;
345 }
346
typeLabel() const347 QString Address::typeLabel() const
348 {
349 QString label;
350 const TypeList list = typeList();
351
352 for (const auto f : list) {
353 if ((type() & f) && (f != Pref)) {
354 label.append(QLatin1Char('/') + typeLabel(f));
355 }
356 }
357 // Remove the first '/'
358 if (!label.isEmpty()) {
359 label.remove(0, 1);
360 }
361 return label;
362 }
363
setPostOfficeBox(const QString & postOfficeBox)364 void Address::setPostOfficeBox(const QString &postOfficeBox)
365 {
366 d->mEmpty = false;
367 d->mPostOfficeBox = postOfficeBox;
368 }
369
postOfficeBox() const370 QString Address::postOfficeBox() const
371 {
372 return d->mPostOfficeBox;
373 }
374
postOfficeBoxLabel()375 QString Address::postOfficeBoxLabel()
376 {
377 return i18n("Post Office Box");
378 }
379
setExtended(const QString & extended)380 void Address::setExtended(const QString &extended)
381 {
382 d->mEmpty = false;
383 d->mExtended = extended;
384 }
385
extended() const386 QString Address::extended() const
387 {
388 return d->mExtended;
389 }
390
extendedLabel()391 QString Address::extendedLabel()
392 {
393 return i18n("Extended Address Information");
394 }
395
setStreet(const QString & street)396 void Address::setStreet(const QString &street)
397 {
398 d->mEmpty = false;
399 d->mStreet = street;
400 }
401
street() const402 QString Address::street() const
403 {
404 return d->mStreet;
405 }
406
streetLabel()407 QString Address::streetLabel()
408 {
409 return i18n("Street");
410 }
411
setLocality(const QString & locality)412 void Address::setLocality(const QString &locality)
413 {
414 d->mEmpty = false;
415 d->mLocality = locality;
416 }
417
locality() const418 QString Address::locality() const
419 {
420 return d->mLocality;
421 }
422
localityLabel()423 QString Address::localityLabel()
424 {
425 return i18n("Locality");
426 }
427
setRegion(const QString & region)428 void Address::setRegion(const QString ®ion)
429 {
430 d->mEmpty = false;
431 d->mRegion = region;
432 }
433
region() const434 QString Address::region() const
435 {
436 return d->mRegion;
437 }
438
regionLabel()439 QString Address::regionLabel()
440 {
441 return i18n("Region");
442 }
443
setPostalCode(const QString & postalCode)444 void Address::setPostalCode(const QString &postalCode)
445 {
446 d->mEmpty = false;
447 d->mPostalCode = postalCode;
448 }
449
postalCode() const450 QString Address::postalCode() const
451 {
452 return d->mPostalCode;
453 }
454
postalCodeLabel()455 QString Address::postalCodeLabel()
456 {
457 return i18n("Postal Code");
458 }
459
setCountry(const QString & country)460 void Address::setCountry(const QString &country)
461 {
462 d->mEmpty = false;
463 d->mCountry = country;
464 }
465
country() const466 QString Address::country() const
467 {
468 return d->mCountry;
469 }
470
countryLabel()471 QString Address::countryLabel()
472 {
473 return i18n("Country");
474 }
475
setLabel(const QString & label)476 void Address::setLabel(const QString &label)
477 {
478 d->mEmpty = false;
479 d->mLabel = label;
480 }
481
label() const482 QString Address::label() const
483 {
484 return d->mLabel;
485 }
486
labelLabel()487 QString Address::labelLabel()
488 {
489 return i18n("Delivery Label");
490 }
491
typeList()492 Address::TypeList Address::typeList()
493 {
494 static TypeList list;
495
496 if (list.isEmpty()) {
497 list << Dom << Intl << Postal << Parcel << Home << Work << Pref;
498 }
499
500 return list;
501 }
502
typeFlagLabel(TypeFlag type)503 QString Address::typeFlagLabel(TypeFlag type)
504 {
505 switch (type) {
506 case Dom:
507 return i18nc("Address is in home country", "Domestic");
508 case Intl:
509 return i18nc("Address is not in home country", "International");
510 case Postal:
511 return i18nc("Address for delivering letters", "Postal");
512 case Parcel:
513 return i18nc("Address for delivering packages", "Parcel");
514 case Home:
515 return i18nc("Home Address", "Home");
516 case Work:
517 return i18nc("Work Address", "Work");
518 case Pref:
519 return i18n("Preferred Address");
520 }
521 return i18nc("another type of address", "Other");
522 }
523
setGeo(const Geo & geo)524 void Address::setGeo(const Geo &geo)
525 {
526 d->mEmpty = false;
527 d->mGeo = geo;
528 }
529
geo() const530 Geo Address::geo() const
531 {
532 return d->mGeo;
533 }
534
toString() const535 QString Address::toString() const
536 {
537 QString str = QLatin1String("Address {\n");
538 str += QStringLiteral(" IsEmpty: %1\n").arg(d->mEmpty ? QStringLiteral("true") : QStringLiteral("false"));
539 str += QStringLiteral(" Id: %1\n").arg(d->mId);
540 str += QStringLiteral(" Type: %1\n").arg(typeLabel(d->mType));
541 str += QStringLiteral(" Post office box: %1\n").arg(d->mPostOfficeBox);
542 str += QStringLiteral(" Extended: %1\n").arg(d->mExtended);
543 str += QStringLiteral(" Street: %1\n").arg(d->mStreet);
544 str += QStringLiteral(" Locality: %1\n").arg(d->mLocality);
545 str += QStringLiteral(" Region: %1\n").arg(d->mRegion);
546 str += QStringLiteral(" Postal code: %1\n").arg(d->mPostalCode);
547 str += QStringLiteral(" Country: %1\n").arg(d->mCountry);
548 str += QStringLiteral(" Label: %1\n").arg(d->mLabel);
549 str += QStringLiteral(" Geo: %1\n").arg(d->mGeo.toString());
550 str += QLatin1String("}\n");
551
552 return str;
553 }
554
addressFormatRc()555 static QString addressFormatRc()
556 {
557 Q_INIT_RESOURCE(kcontacts); // must be called outside of a namespace
558 return QStringLiteral(":/org.kde.kcontacts/addressformatrc");
559 }
560
formattedAddress(const QString & realName,const QString & orgaName) const561 QString Address::formattedAddress(const QString &realName, const QString &orgaName) const
562 {
563 KCountry countryCode;
564 QString addrTemplate;
565 QString ret;
566
567 if (country().size() == 2) {
568 countryCode = KCountry::fromAlpha2(country());
569 }
570 if (!countryCode.isValid()) {
571 countryCode = KCountry::fromName(country());
572 }
573 // fall back to our own country
574 if (!countryCode.isValid()) {
575 countryCode = KCountry::fromQLocale(QLocale().country());
576 }
577 static const KConfig entry(addressFormatRc());
578
579 KConfigGroup group = entry.group(countryCode.alpha2());
580 // decide whether this needs special business address formatting
581 if (orgaName.isEmpty()) {
582 addrTemplate = group.readEntry("AddressFormat");
583 } else {
584 addrTemplate = group.readEntry("BusinessAddressFormat");
585 if (addrTemplate.isEmpty()) {
586 addrTemplate = group.readEntry("AddressFormat");
587 }
588 }
589
590 // in the case there's no format found at all, default to what we've always
591 // used:
592 if (addrTemplate.isEmpty()) {
593 qCWarning(KCONTACTS_LOG) << "address format database incomplete"
594 << "(no format for locale" << countryCode.alpha2() << "found). Using default address formatting.";
595 addrTemplate = QStringLiteral("%0(%n\\n)%0(%cm\\n)%0(%s\\n)%0(PO BOX %p\\n)%0(%l%w%r)%,%z");
596 }
597
598 // scan
599 parseAddressTemplateSection(addrTemplate, ret, realName, orgaName, *this);
600
601 // now add the country line if needed (formatting this time according to
602 // the rules of our own system country )
603 if (!country().isEmpty()) {
604 const QString countryName = countryCode.isValid() ? countryCode.name() : country();
605
606 // Don't include line breaks if country is the only text
607 if (ret.isEmpty()) {
608 return countryName;
609 }
610
611 KConfigGroup group = entry.group(KCountry::fromQLocale(QLocale().country()).alpha2());
612 QString cpos = group.readEntry("AddressCountryPosition");
613 if (QLatin1String("BELOW") == cpos || cpos.isEmpty()) {
614 ret = ret + QLatin1String("\n\n") + countryName.toUpper();
615 } else if (QLatin1String("below") == cpos) {
616 ret = ret + QLatin1String("\n\n") + countryName;
617 } else if (QLatin1String("ABOVE") == cpos) {
618 ret = countryName.toUpper() + QLatin1String("\n\n") + ret;
619 } else if (QLatin1String("above") == cpos) {
620 ret = countryName + QLatin1String("\n\n") + ret;
621 }
622 }
623
624 return ret;
625 }
626
627 #if KCONTACTS_BUILD_DEPRECATED_SINCE(5, 89)
countryToISO(const QString & cname)628 QString Address::countryToISO(const QString &cname)
629 {
630 return KCountry::fromName(cname).alpha2().toLower();
631 }
632 #endif
633
634 #if KCONTACTS_BUILD_DEPRECATED_SINCE(5, 89)
ISOtoCountry(const QString & ISOname)635 QString Address::ISOtoCountry(const QString &ISOname)
636 {
637 const auto c = KCountry::fromAlpha2(ISOname);
638 return c.isValid() ? c.name() : ISOname;
639 }
640 #endif
641
642 // clang-format off
operator <<(QDataStream & s,const Address & addr)643 QDataStream &KContacts::operator<<(QDataStream &s, const Address &addr)
644 {
645 return s << addr.d->mId << (uint)addr.d->mType << addr.d->mPostOfficeBox
646 << addr.d->mExtended << addr.d->mStreet << addr.d->mLocality
647 << addr.d->mRegion << addr.d->mPostalCode << addr.d->mCountry
648 << addr.d->mLabel << addr.d->mEmpty << addr.d->mGeo;
649 }
650
operator >>(QDataStream & s,Address & addr)651 QDataStream &KContacts::operator>>(QDataStream &s, Address &addr)
652 {
653 uint type;
654 s >> addr.d->mId >> type >> addr.d->mPostOfficeBox >> addr.d->mExtended
655 >> addr.d->mStreet >> addr.d->mLocality >> addr.d->mRegion
656 >> addr.d->mPostalCode >> addr.d->mCountry >> addr.d->mLabel
657 >> addr.d->mEmpty >> addr.d->mGeo;
658
659 addr.d->mType = Address::Type(type);
660
661 return s;
662 }
663 // clang-format on
664