File indexing completed on 2024-05-12 05:46:55

0001 /* This file is part of the KDE File Manager
0002 
0003    Copyright (C) 1998-2000 Waldo Bastian (bastian@kde.org)
0004    Copyright (C) 2000,2001 Dawit Alemayehu (adawit@kde.org)
0005 
0006    Permission is hereby granted, free of charge, to any person obtaining a copy
0007    of this software and associated documentation files (the "Software"), to deal
0008    in the Software without restriction, including without limitation the rights
0009    to use, copy, modify, merge, publish, distribute, and/or sell copies of the
0010    Software, and to permit persons to whom the Software is furnished to do so,
0011    subject to the following conditions:
0012 
0013    The above copyright notice and this permission notice shall be included in
0014    all copies or substantial portions of the Software.
0015 
0016    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
0017    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
0018    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
0019    AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
0020    ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
0021    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
0022 */
0023 //----------------------------------------------------------------------------
0024 //
0025 // KDE File Manager -- HTTP Cookies
0026 
0027 //
0028 // The cookie protocol is a mess. RFC2109 is a joke since nobody seems to
0029 // use it. Apart from that it is badly written.
0030 // We try to implement Netscape Cookies and try to behave us according to
0031 // RFC2109 as much as we can.
0032 //
0033 // We assume cookies do not contain any spaces (Netscape spec.)
0034 // According to RFC2109 this is allowed though.
0035 //
0036 
0037 #include "kcookiejar.h"
0038 
0039 #include <kconfig.h>
0040 #include <kconfiggroup.h>
0041 #include <qsavefile.h>
0042 #include <QDebug>
0043 
0044 #include <QString>
0045 #include <QFile>
0046 #include <QRegExp>
0047 #include <QTextStream>
0048 #include <QLocale>
0049 #include <QUrl>
0050 
0051 Q_LOGGING_CATEGORY(KIO_COOKIEJAR, "kf5.kio.cookiejar")
0052 
0053 // BR87227
0054 // Waba: Should the number of cookies be limited?
0055 // I am not convinced of the need of such limit
0056 // Mozilla seems to limit to 20 cookies / domain
0057 // but it is unclear which policy it uses to expire
0058 // cookies when it exceeds that amount
0059 #undef MAX_COOKIE_LIMIT
0060 
0061 #define MAX_COOKIES_PER_HOST 25
0062 #define READ_BUFFER_SIZE 8192
0063 #define IP_ADDRESS_EXPRESSION "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
0064 
0065 // Note with respect to QLatin1String( )....
0066 // Cookies are stored as 8 bit data and passed to kio_http as Latin1
0067 // regardless of their actual encoding.
0068 #define QL1S(x)   QLatin1String(x)
0069 #define QL1C(x)   QLatin1Char(x)
0070 
0071 static QString removeWeekday(const QString &value)
0072 {
0073     const int index = value.indexOf(QL1C(' '));
0074     if (index > -1) {
0075         int pos = 0;
0076         const QStringRef weekday = value.leftRef(index);
0077         const QLocale cLocale = QLocale::c();
0078         for (int i = 1; i < 8; ++i) {
0079             // No need to check for long names since the short names are
0080             // prefixes of the long names
0081             if (weekday.startsWith(cLocale.dayName(i, QLocale::ShortFormat), Qt::CaseInsensitive)) {
0082                 pos = index + 1;
0083                 break;
0084             }
0085         }
0086         if (pos > 0) {
0087             return value.mid(pos);
0088         }
0089     }
0090     return value;
0091 }
0092 
0093 static QDateTime parseDate(const QString &_value)
0094 {
0095     // Handle sites sending invalid weekday as part of the date. #298660
0096     const QString value(removeWeekday(_value));
0097 
0098     // Check if expiration date matches RFC dates as specified under
0099     // RFC 2616 sec 3.3.1 & RFC 6265 sec 4.1.1
0100     QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date);
0101 
0102     if (!dt.isValid()) {
0103         static const char *const date_formats[] = {
0104             // Other formats documented in RFC 2616 sec 3.3.1
0105             // Note: the RFC says timezone information MUST be "GMT", hence the hardcoded timezone string
0106             "MMM dd HH:mm:ss yyyy",       /* ANSI C's asctime() format (#145244): Jan 01 00:00:00 1970 GMT */
0107             "dd-MMM-yy HH:mm:ss 'GMT'",   /* RFC 850 date: 06-Dec-39 00:30:42 GMT */
0108             // Non-standard formats
0109             "MMM dd yyyy HH:mm:ss",       /* A variation on ANSI C format seen @ amazon.com: Jan 01 1970 00:00:00 GMT */
0110             "dd-MMM-yyyy HH:mm:ss 'GMT'", /* cookies.test: Y2K38 problem: 06-Dec-2039 00:30:42 GMT */
0111             "MMM dd HH:mm:ss yyyy 'GMT'", /* cookies.test: Non-standard expiration dates: Sep 12 07:00:00 2020 GMT */
0112             "MMM dd yyyy HH:mm:ss 'GMT'", /* cookies.test: Non-standard expiration dates: Sep 12 2020 07:00:00 GMT */
0113             nullptr
0114         };
0115 
0116         // Only English month names are allowed, thus use the C locale.
0117         const QLocale cLocale = QLocale::c();
0118         for (int i = 0; date_formats[i]; ++i) {
0119             dt = cLocale.toDateTime(value, QL1S(date_formats[i]));
0120             if (dt.isValid()) {
0121                 dt.setTimeSpec(Qt::UTC);
0122                 break;
0123             }
0124         }
0125     }
0126 
0127     return dt.toUTC();  // Per RFC 2616 sec 3.3.1 always convert to UTC.
0128 }
0129 
0130 static qint64 toEpochSecs(const QDateTime &dt)
0131 {
0132     return (dt.toMSecsSinceEpoch() / 1000); // convert to seconds...
0133 }
0134 
0135 static qint64 epoch()
0136 {
0137     return toEpochSecs(QDateTime::currentDateTimeUtc());
0138 }
0139 
0140 QString KCookieJar::adviceToStr(KCookieAdvice _advice)
0141 {
0142     switch (_advice) {
0143     case KCookieAccept: return QStringLiteral("Accept");
0144     case KCookieAcceptForSession: return QStringLiteral("AcceptForSession");
0145     case KCookieReject: return QStringLiteral("Reject");
0146     case KCookieAsk: return QStringLiteral("Ask");
0147     default: return QStringLiteral("Dunno");
0148     }
0149 }
0150 
0151 KCookieAdvice KCookieJar::strToAdvice(const QString &_str)
0152 {
0153     if (_str.isEmpty()) {
0154         return KCookieDunno;
0155     }
0156 
0157     QString advice = _str.toLower();
0158 
0159     if (advice == QL1S("accept")) {
0160         return KCookieAccept;
0161     } else if (advice == QL1S("acceptforsession")) {
0162         return KCookieAcceptForSession;
0163     } else if (advice == QL1S("reject")) {
0164         return KCookieReject;
0165     } else if (advice == QL1S("ask")) {
0166         return KCookieAsk;
0167     }
0168 
0169     return KCookieDunno;
0170 }
0171 
0172 // KHttpCookie
0173 ///////////////////////////////////////////////////////////////////////////
0174 
0175 //
0176 // Cookie constructor
0177 //
0178 KHttpCookie::KHttpCookie(const QString &_host,
0179                          const QString &_domain,
0180                          const QString &_path,
0181                          const QString &_name,
0182                          const QString &_value,
0183                          qint64 _expireDate,
0184                          int _protocolVersion,
0185                          bool _secure,
0186                          bool _httpOnly,
0187                          bool _explicitPath) :
0188     mHost(_host),
0189     mDomain(_domain),
0190     mPath(_path.isEmpty() ? QString() : _path),
0191     mName(_name),
0192     mValue(_value),
0193     mExpireDate(_expireDate),
0194     mProtocolVersion(_protocolVersion),
0195     mSecure(_secure),
0196     mCrossDomain(false),
0197     mHttpOnly(_httpOnly),
0198     mExplicitPath(_explicitPath),
0199     mUserSelectedAdvice(KCookieDunno)
0200 {
0201 }
0202 
0203 //
0204 // Checks if a cookie has been expired
0205 //
0206 bool KHttpCookie::isExpired(qint64 currentDate) const
0207 {
0208     if (currentDate == -1) {
0209         currentDate = epoch();
0210     }
0211 
0212     return (mExpireDate != 0) && (mExpireDate < currentDate);
0213 }
0214 
0215 //
0216 // Returns a string for a HTTP-header
0217 //
0218 QString KHttpCookie::cookieStr(bool useDOMFormat) const
0219 {
0220     QString result;
0221 
0222     if (useDOMFormat || (mProtocolVersion == 0)) {
0223         if (mName.isEmpty()) {
0224             result = mValue;
0225         } else {
0226             result = mName + QL1C('=') + mValue;
0227         }
0228     } else {
0229         result = mName + QL1C('=') + mValue;
0230         if (mExplicitPath) {
0231             result += QL1S("; $Path=\"") + mPath + QL1C('"');
0232         }
0233         if (!mDomain.isEmpty()) {
0234             result += QL1S("; $Domain=\"") + mDomain + QL1C('"');
0235         }
0236         if (!mPorts.isEmpty()) {
0237             if (mPorts.length() == 2 && mPorts.at(0) == -1) {
0238                 result += QL1S("; $Port");
0239             } else {
0240                 QString portNums;
0241                 for (int port : qAsConst(mPorts)) {
0242                     portNums += QString::number(port) + QL1C(' ');
0243                 }
0244                 result += QL1S("; $Port=\"") + portNums.trimmed() + QL1C('"');
0245             }
0246         }
0247     }
0248     return result;
0249 }
0250 
0251 //
0252 // Returns whether this cookie should be send to this location.
0253 bool KHttpCookie::match(const QString &fqdn, const QStringList &domains,
0254                         const QString &path, int port) const
0255 {
0256     // Cookie domain match check
0257     if (mDomain.isEmpty()) {
0258         if (fqdn != mHost) {
0259             return false;
0260         }
0261     } else if (!domains.contains(mDomain)) {
0262         if (mDomain[0] == QL1C('.')) {
0263             return false;
0264         }
0265 
0266         // Maybe the domain needs an extra dot.
0267         const QString domain = QL1C('.') + mDomain;
0268         if (!domains.contains(domain))
0269             if (fqdn != mDomain) {
0270                 return false;
0271             }
0272     } else if (mProtocolVersion != 0 && port != -1 &&
0273                !mPorts.isEmpty() && !mPorts.contains(port)) {
0274         return false;
0275     }
0276 
0277     // Cookie path match check
0278     if (mPath.isEmpty()) {
0279         return true;
0280     }
0281 
0282     // According to the netscape spec http://www.acme.com/foobar,
0283     // http://www.acme.com/foo.bar and http://www.acme.com/foo/bar
0284     // should all match http://www.acme.com/foo...
0285     // We only match http://www.acme.com/foo/bar
0286     if (path.startsWith(mPath) &&
0287             (
0288                 (path.length() == mPath.length()) ||   // Paths are exact match
0289                 mPath.endsWith(QL1C('/')) ||          // mPath ended with a slash
0290                 (path[mPath.length()] == QL1C('/'))        // A slash follows
0291             )) {
0292         return true;    // Path of URL starts with cookie-path
0293     }
0294 
0295     return false;
0296 }
0297 
0298 // KCookieJar
0299 ///////////////////////////////////////////////////////////////////////////
0300 
0301 //
0302 // Constructs a new cookie jar
0303 //
0304 // One jar should be enough for all cookies.
0305 //
0306 KCookieJar::KCookieJar()
0307 {
0308     m_globalAdvice = KCookieDunno;
0309     m_configChanged = false;
0310     m_cookiesChanged = false;
0311 
0312     KConfig cfg(QStringLiteral("kf5/kcookiejar/domain_info"), KConfig::NoGlobals, QStandardPaths::GenericDataLocation);
0313     KConfigGroup group(&cfg, QString());
0314     m_gTLDs = QSet<QString>::fromList(group.readEntry("gTLDs", QStringList()));
0315     m_twoLevelTLD = QSet<QString>::fromList(group.readEntry("twoLevelTLD", QStringList()));
0316 }
0317 
0318 //
0319 // Destructs the cookie jar
0320 //
0321 // Poor little cookies, they will all be eaten by the cookie monster!
0322 //
0323 KCookieJar::~KCookieJar()
0324 {
0325     qDeleteAll(m_cookieDomains);
0326     // Not much to do here
0327 }
0328 
0329 // cookiePtr is modified: the window ids of the existing cookie in the list are added to it
0330 static void removeDuplicateFromList(KHttpCookieList *list, KHttpCookie &cookiePtr, bool nameMatchOnly = false, bool updateWindowId = false)
0331 {
0332     QString domain1 = cookiePtr.domain();
0333     if (domain1.isEmpty()) {
0334         domain1 = cookiePtr.host();
0335     }
0336 
0337     QMutableListIterator<KHttpCookie> cookieIterator(*list);
0338     while (cookieIterator.hasNext()) {
0339         const KHttpCookie &cookie = cookieIterator.next();
0340         QString domain2 = cookie.domain();
0341         if (domain2.isEmpty()) {
0342             domain2 = cookie.host();
0343         }
0344 
0345         if (cookiePtr.name() == cookie.name() &&
0346                 (nameMatchOnly || (domain1 == domain2 && cookiePtr.path() == cookie.path()))) {
0347             if (updateWindowId) {
0348                 for (WId windowId : cookie.windowIds()) {
0349                     if (windowId && (!cookiePtr.windowIds().contains(windowId))) {
0350                         cookiePtr.windowIds().append(windowId);
0351                     }
0352                 }
0353             }
0354             cookieIterator.remove();
0355             break;
0356         }
0357     }
0358 }
0359 
0360 //
0361 // Looks for cookies in the cookie jar which are appropriate for _url.
0362 // Returned is a string containing all appropriate cookies in a format
0363 // which can be added to a HTTP-header without any additional processing.
0364 //
0365 QString KCookieJar::findCookies(const QString &_url, bool useDOMFormat, WId windowId, KHttpCookieList *pendingCookies)
0366 {
0367     QString cookieStr, fqdn, path;
0368     QStringList domains;
0369     int port = -1;
0370 
0371     if (!parseUrl(_url, fqdn, path, &port)) {
0372         return cookieStr;
0373     }
0374 
0375     const bool secureRequest = (_url.startsWith(QL1S("https://"), Qt::CaseInsensitive) ||
0376                                 _url.startsWith(QL1S("webdavs://"), Qt::CaseInsensitive));
0377     if (port == -1) {
0378         port = (secureRequest ? 443 : 80);
0379     }
0380 
0381     extractDomains(fqdn, domains);
0382 
0383     KHttpCookieList allCookies;
0384     for (QStringList::ConstIterator it = domains.constBegin(), itEnd = domains.constEnd();; ++it) {
0385         KHttpCookieList *cookieList = nullptr;
0386         if (it == itEnd) {
0387             cookieList = pendingCookies; // Add pending cookies
0388             pendingCookies = nullptr;
0389             if (!cookieList) {
0390                 break;
0391             }
0392         } else {
0393             if ((*it).isNull()) {
0394                 cookieList = m_cookieDomains.value(QL1S(""));
0395             } else {
0396                 cookieList = m_cookieDomains.value(*it);
0397             }
0398 
0399             if (!cookieList) {
0400                 continue;    // No cookies for this domain
0401             }
0402         }
0403 
0404         QMutableListIterator<KHttpCookie> cookieIt(*cookieList);
0405         while (cookieIt.hasNext()) {
0406             KHttpCookie &cookie = cookieIt.next();
0407             if (cookieAdvice(cookie) == KCookieReject) {
0408                 continue;
0409             }
0410 
0411             if (!cookie.match(fqdn, domains, path, port)) {
0412                 continue;
0413             }
0414 
0415             if (cookie.isSecure() && !secureRequest) {
0416                 continue;
0417             }
0418 
0419             if (cookie.isHttpOnly() && useDOMFormat) {
0420                 continue;
0421             }
0422 
0423             // Do not send expired cookies.
0424             if (cookie.isExpired()) {
0425                 // NOTE: there is no need to delete the cookie here because the
0426                 // cookieserver will invoke its saveCookieJar function as a result
0427                 // of the state change below. This will then result in the cookie
0428                 // being deleting at that point.
0429                 m_cookiesChanged = true;
0430                 continue;
0431             }
0432 
0433             if (windowId && (cookie.windowIds().indexOf(windowId) == -1)) {
0434                 cookie.windowIds().append(windowId);
0435             }
0436 
0437             if (it == itEnd) { // Only needed when processing pending cookies
0438                 removeDuplicateFromList(&allCookies, cookie);
0439             }
0440 
0441             allCookies.append(cookie);
0442         }
0443 
0444         if (it == itEnd) {
0445             break;    // Finished.
0446         }
0447     }
0448 
0449     int protVersion = 0;
0450     for (const KHttpCookie &cookie : qAsConst(allCookies)) {
0451         if (cookie.protocolVersion() > protVersion) {
0452             protVersion = cookie.protocolVersion();
0453         }
0454     }
0455 
0456     if (!allCookies.isEmpty()) {
0457         if (!useDOMFormat) {
0458             cookieStr = QStringLiteral("Cookie: ");
0459         }
0460 
0461         if (protVersion > 0) {
0462             cookieStr = cookieStr + QLatin1String("$Version=") + QString::number(protVersion) + QLatin1String("; ");
0463         }
0464 
0465         for (const KHttpCookie &cookie : qAsConst(allCookies)) {
0466             cookieStr = cookieStr + cookie.cookieStr(useDOMFormat) + QStringLiteral("; ");
0467         }
0468 
0469         cookieStr.chop(2); // Remove the trailing '; '
0470     }
0471 
0472     return cookieStr;
0473 }
0474 
0475 //
0476 // This function parses a string like 'my_name="my_value";' and returns
0477 // 'my_name' in Name and 'my_value' in Value.
0478 //
0479 // A pointer to the end of the parsed part is returned.
0480 // This pointer points either to:
0481 // '\0' - The end of the string has reached.
0482 // ';'  - Another my_name="my_value" pair follows
0483 // ','  - Another cookie follows
0484 // '\n' - Another header follows
0485 static const char *parseNameValue(const char *header,
0486                                   QString &Name,
0487                                   QString &Value,
0488                                   bool keepQuotes = false,
0489                                   bool rfcQuotes = false)
0490 {
0491     const char *s = header;
0492     // Parse 'my_name' part
0493     for (; (*s != '='); s++) {
0494         if ((*s == '\0') || (*s == ';') || (*s == '\n')) {
0495             // No '=' sign -> use string as the value, name is empty
0496             // (behavior found in Mozilla and IE)
0497             Name = QL1S("");
0498             Value = QL1S(header);
0499             Value.truncate(s - header);
0500             Value = Value.trimmed();
0501             return s;
0502         }
0503     }
0504 
0505     Name = QL1S(header);
0506     Name.truncate(s - header);
0507     Name = Name.trimmed();
0508 
0509     // *s == '='
0510     s++;
0511 
0512     // Skip any whitespace
0513     for (; (*s == ' ') || (*s == '\t'); s++) {
0514         if ((*s == '\0') || (*s == ';') || (*s == '\n')) {
0515             // End of Name
0516             Value = QLatin1String("");
0517             return s;
0518         }
0519     }
0520 
0521     if ((rfcQuotes || !keepQuotes) && (*s == '\"')) {
0522         // Parse '"my_value"' part (quoted value)
0523         if (keepQuotes) {
0524             header = s++;
0525         } else {
0526             header = ++s;    // skip "
0527         }
0528         for (; (*s != '\"'); s++) {
0529             if ((*s == '\0') || (*s == '\n')) {
0530                 // End of Name
0531                 Value = QL1S(header);
0532                 Value.truncate(s - header);
0533                 return s;
0534             }
0535         }
0536         Value = QL1S(header);
0537         // *s == '\"';
0538         if (keepQuotes) {
0539             Value.truncate(++s - header);
0540         } else {
0541             Value.truncate(s++ - header);
0542         }
0543 
0544         // Skip any remaining garbage
0545         for (;; s++) {
0546             if ((*s == '\0') || (*s == ';') || (*s == '\n')) {
0547                 break;
0548             }
0549         }
0550     } else {
0551         // Parse 'my_value' part (unquoted value)
0552         header = s;
0553         while ((*s != '\0') && (*s != ';') && (*s != '\n')) {
0554             s++;
0555         }
0556         // End of Name
0557         Value = QL1S(header);
0558         Value.truncate(s - header);
0559         Value = Value.trimmed();
0560     }
0561     return s;
0562 
0563 }
0564 
0565 void KCookieJar::stripDomain(const QString &_fqdn, QString &_domain) const
0566 {
0567     QStringList domains;
0568     extractDomains(_fqdn, domains);
0569     if (domains.count() > 3) {
0570         _domain = domains[3];
0571     } else if (!domains.isEmpty()) {
0572         _domain = domains[0];
0573     } else {
0574         _domain = QL1S("");
0575     }
0576 }
0577 
0578 QString KCookieJar::stripDomain(const KHttpCookie &cookie) const
0579 {
0580     QString domain; // We file the cookie under this domain.
0581     if (cookie.domain().isEmpty()) {
0582         stripDomain(cookie.host(), domain);
0583     } else {
0584         domain = cookie.domain();
0585     }
0586     return domain;
0587 }
0588 
0589 bool KCookieJar::parseUrl(const QString &_url, QString &_fqdn, QString &_path, int *port)
0590 {
0591     QUrl kurl(_url);
0592     if (!kurl.isValid() || kurl.scheme().isEmpty()) {
0593         return false;
0594     }
0595 
0596     _fqdn = kurl.host().toLower();
0597     // Cookie spoofing protection.  Since there is no way a path separator,
0598     // a space or the escape encoding character is allowed in the hostname
0599     // according to RFC 2396, reject attempts to include such things there!
0600     if (_fqdn.contains(QL1C('/')) || _fqdn.contains(QL1C('%'))) {
0601         return false;    // deny everything!!
0602     }
0603 
0604     // Set the port number from the protocol when one is found...
0605     if (port) {
0606         *port = kurl.port();
0607     }
0608 
0609     _path = kurl.path();
0610     if (_path.isEmpty()) {
0611         _path = QStringLiteral("/");
0612     }
0613 
0614     return true;
0615 }
0616 
0617 // not static because it uses m_twoLevelTLD
0618 void KCookieJar::extractDomains(const QString &_fqdn,
0619                                 QStringList &_domains) const
0620 {
0621     if (_fqdn.isEmpty()) {
0622         _domains.append(QStringLiteral("localhost"));
0623         return;
0624     }
0625 
0626     // Return numeric IPv6 addresses as is...
0627     if (_fqdn[0] == QL1C('[')) {
0628         _domains.append(_fqdn);
0629         return;
0630     }
0631     // Return numeric IPv4 addresses as is...
0632     if (_fqdn[0] >= QL1C('0') && _fqdn[0] <= QL1C('9') && _fqdn.indexOf(QRegExp(QStringLiteral(IP_ADDRESS_EXPRESSION))) > -1) {
0633         _domains.append(_fqdn);
0634         return;
0635     }
0636 
0637     // Always add the FQDN at the start of the list for
0638     // hostname == cookie-domainname checks!
0639     _domains.append(_fqdn);
0640     _domains.append(QL1C('.') + _fqdn);
0641 
0642     QStringList partList = _fqdn.split(QL1C('.'), QString::SkipEmptyParts);
0643 
0644     if (!partList.isEmpty()) {
0645         partList.erase(partList.begin());    // Remove hostname
0646     }
0647 
0648     while (partList.count()) {
0649 
0650         if (partList.count() == 1) {
0651             break;    // We only have a TLD left.
0652         }
0653 
0654         if ((partList.count() == 2) && m_twoLevelTLD.contains(partList[1].toLower())) {
0655             // This domain uses two-level TLDs in the form xxxx.yy
0656             break;
0657         }
0658 
0659         if ((partList.count() == 2) && (partList[1].length() == 2)) {
0660             // If this is a TLD, we should stop. (e.g. co.uk)
0661             // We assume this is a TLD if it ends with .xx.yy or .x.yy
0662             if (partList[0].length() <= 2) {
0663                 break;    // This is a TLD.
0664             }
0665 
0666             // Catch some TLDs that we miss with the previous check
0667             // e.g. com.au, org.uk, mil.co
0668             if (m_gTLDs.contains(partList[0].toLower())) {
0669                 break;
0670             }
0671         }
0672 
0673         QString domain = partList.join(QLatin1Char('.'));
0674         _domains.append(domain);
0675         _domains.append(QL1C('.') + domain);
0676         partList.erase(partList.begin()); // Remove part
0677     }
0678 }
0679 
0680 //
0681 // This function parses cookie_headers and returns a linked list of
0682 // KHttpCookie objects for all cookies found in cookie_headers.
0683 // If no cookies could be found 0 is returned.
0684 //
0685 // cookie_headers should be a concatenation of all lines of a HTTP-header
0686 // which start with "Set-Cookie". The lines should be separated by '\n's.
0687 //
0688 KHttpCookieList KCookieJar::makeCookies(const QString &_url,
0689                                         const QByteArray &cookie_headers,
0690                                         WId windowId)
0691 {
0692     QString fqdn, path;
0693 
0694     if (!parseUrl(_url, fqdn, path)) {
0695         return KHttpCookieList();    // Error parsing _url
0696     }
0697 
0698     QString Name, Value;
0699     KHttpCookieList cookieList, cookieList2;
0700 
0701     bool isRFC2965 = false;
0702     bool crossDomain = false;
0703     const char *cookieStr = cookie_headers.constData();
0704 
0705     QString defaultPath;
0706     const int index = path.lastIndexOf(QL1C('/'));
0707     if (index > 0) {
0708         defaultPath = path.left(index);
0709     }
0710 
0711     // Check for cross-domain flag from kio_http
0712     if (qstrncmp(cookieStr, "Cross-Domain\n", 13) == 0) {
0713         cookieStr += 13;
0714         crossDomain = true;
0715     }
0716 
0717     //  The hard stuff :)
0718     for (;;) {
0719         // check for "Set-Cookie"
0720         if (qstrnicmp(cookieStr, "Set-Cookie:", 11) == 0) {
0721             cookieStr = parseNameValue(cookieStr + 11, Name, Value, true);
0722 
0723             // Host = FQDN
0724             // Default domain = ""
0725             // Default path according to rfc2109
0726 
0727             KHttpCookie cookie(fqdn, QL1S(""), defaultPath, Name, Value);
0728             if (windowId) {
0729                 cookie.mWindowIds.append(windowId);
0730             }
0731             cookie.mCrossDomain = crossDomain;
0732 
0733             // Insert cookie in chain
0734             cookieList.append(cookie);
0735         } else if (qstrnicmp(cookieStr, "Set-Cookie2:", 12) == 0) {
0736             // Attempt to follow rfc2965
0737             isRFC2965 = true;
0738             cookieStr = parseNameValue(cookieStr + 12, Name, Value, true, true);
0739 
0740             // Host = FQDN
0741             // Default domain = ""
0742             // Default path according to rfc2965
0743 
0744             KHttpCookie cookie(fqdn, QL1S(""), defaultPath, Name, Value);
0745             if (windowId) {
0746                 cookie.mWindowIds.append(windowId);
0747             }
0748             cookie.mCrossDomain = crossDomain;
0749 
0750             // Insert cookie in chain
0751             cookieList2.append(cookie);
0752         } else {
0753             // This is not the start of a cookie header, skip till next line.
0754             while (*cookieStr && *cookieStr != '\n') {
0755                 cookieStr++;
0756             }
0757 
0758             if (*cookieStr == '\n') {
0759                 cookieStr++;
0760             }
0761 
0762             if (!*cookieStr) {
0763                 break;    // End of cookie_headers
0764             } else {
0765                 continue;    // end of this header, continue with next.
0766             }
0767         }
0768 
0769         while ((*cookieStr == ';') || (*cookieStr == ' ')) {
0770             cookieStr++;
0771 
0772             // Name-Value pair follows
0773             cookieStr = parseNameValue(cookieStr, Name, Value);
0774             KHttpCookie &lastCookie = (isRFC2965 ? cookieList2.last() : cookieList.last());
0775 
0776             if (Name.compare(QL1S("domain"), Qt::CaseInsensitive) == 0) {
0777                 QString dom = Value.toLower();
0778                 // RFC2965 3.2.2: If an explicitly specified value does not
0779                 // start with a dot, the user agent supplies a leading dot
0780                 if (dom.length() > 0 && dom[0] != QL1C('.')) {
0781                     dom.prepend(QL1C('.'));
0782                 }
0783                 // remove a trailing dot
0784                 if (dom.length() > 2 && dom[dom.length() - 1] == QL1C('.')) {
0785                     dom.chop(1);
0786                 }
0787 
0788                 if (dom.count(QL1C('.')) > 1 || dom == QLatin1String(".local")) {
0789                     lastCookie.mDomain = dom;
0790                 }
0791             } else if (Name.compare(QL1S("max-age"), Qt::CaseInsensitive) == 0) {
0792                 int max_age = Value.toInt();
0793                 if (max_age == 0) {
0794                     lastCookie.mExpireDate = 1;
0795                 } else {
0796                     lastCookie.mExpireDate = toEpochSecs(QDateTime::currentDateTimeUtc().addSecs(max_age));
0797                 }
0798             } else if (Name.compare(QL1S("expires"), Qt::CaseInsensitive) == 0) {
0799                 const QDateTime dt = parseDate(Value);
0800 
0801                 if (dt.isValid()) {
0802                     lastCookie.mExpireDate = toEpochSecs(dt);
0803                     if (lastCookie.mExpireDate == 0) {
0804                         lastCookie.mExpireDate = 1;
0805                     }
0806                 }
0807             } else if (Name.compare(QL1S("path"), Qt::CaseInsensitive) == 0) {
0808                 if (Value.isEmpty()) {
0809                     lastCookie.mPath.clear();    // Catch "" <> QString()
0810                 } else {
0811                     lastCookie.mPath = QUrl::fromPercentEncoding(Value.toLatin1());
0812                 }
0813                 lastCookie.mExplicitPath = true;
0814             } else if (Name.compare(QL1S("version"), Qt::CaseInsensitive) == 0) {
0815                 lastCookie.mProtocolVersion = Value.toInt();
0816             } else if (Name.compare(QL1S("secure"), Qt::CaseInsensitive) == 0 ||
0817                        (Name.isEmpty() && Value.compare(QL1S("secure"), Qt::CaseInsensitive) == 0)) {
0818                 lastCookie.mSecure = true;
0819             } else if (Name.compare(QL1S("httponly"), Qt::CaseInsensitive) == 0 ||
0820                        (Name.isEmpty() && Value.compare(QL1S("httponly"), Qt::CaseInsensitive) == 0)) {
0821                 lastCookie.mHttpOnly = true;
0822             } else if (isRFC2965 && (Name.compare(QL1S("port"), Qt::CaseInsensitive) == 0 ||
0823                                      (Name.isEmpty() && Value.compare(QL1S("port"), Qt::CaseInsensitive) == 0))) {
0824                 // Based on the port selection rule of RFC 2965 section 3.3.4...
0825                 if (Name.isEmpty()) {
0826                     // We intentionally append a -1 first in order to distinguish
0827                     // between only a 'Port' vs a 'Port="80 443"' in the sent cookie.
0828                     lastCookie.mPorts.append(-1);
0829                     const bool secureRequest = (_url.startsWith(QL1S("https://"), Qt::CaseInsensitive) ||
0830                                                 _url.startsWith(QL1S("webdavs://"), Qt::CaseInsensitive));
0831                     if (secureRequest) {
0832                         lastCookie.mPorts.append(443);
0833                     } else {
0834                         lastCookie.mPorts.append(80);
0835                     }
0836                 } else {
0837                     bool ok;
0838                     const QStringList portNums = Value.split(QL1C(' '), QString::SkipEmptyParts);
0839                     for (const QString &portNum : portNums) {
0840                         const int port = portNum.toInt(&ok);
0841                         if (ok) {
0842                             lastCookie.mPorts.append(port);
0843                         }
0844                     }
0845                 }
0846             }
0847         }
0848 
0849         if (*cookieStr == '\0') {
0850             break;    // End of header
0851         }
0852 
0853         // Skip ';' or '\n'
0854         cookieStr++;
0855     }
0856 
0857     // RFC2965 cookies come last so that they override netscape cookies.
0858     while (!cookieList2.isEmpty()) {
0859         KHttpCookie &lastCookie = cookieList2.first();
0860         removeDuplicateFromList(&cookieList, lastCookie, true);
0861         cookieList.append(lastCookie);
0862         cookieList2.removeFirst();
0863     }
0864 
0865     return cookieList;
0866 }
0867 
0868 /**
0869 * Parses cookie_domstr and returns a linked list of KHttpCookie objects.
0870 * cookie_domstr should be a semicolon-delimited list of "name=value"
0871 * pairs. Any whitespace before "name" or around '=' is discarded.
0872 * If no cookies are found, 0 is returned.
0873 */
0874 KHttpCookieList KCookieJar::makeDOMCookies(const QString &_url,
0875         const QByteArray &cookie_domstring,
0876         WId windowId)
0877 {
0878     // A lot copied from above
0879     KHttpCookieList cookieList;
0880 
0881     const char *cookieStr = cookie_domstring.data();
0882     QString fqdn;
0883     QString path;
0884 
0885     if (!parseUrl(_url, fqdn, path)) {
0886         // Error parsing _url
0887         return KHttpCookieList();
0888     }
0889 
0890     QString Name;
0891     QString Value;
0892     //  This time it's easy
0893     while (*cookieStr) {
0894         cookieStr = parseNameValue(cookieStr, Name, Value);
0895 
0896         // Host = FQDN
0897         // Default domain = ""
0898         // Default path = ""
0899         KHttpCookie cookie(fqdn, QString(), QString(),
0900                            Name, Value);
0901         if (windowId) {
0902             cookie.mWindowIds.append(windowId);
0903         }
0904 
0905         cookieList.append(cookie);
0906 
0907         if (*cookieStr != '\0') {
0908             cookieStr++;    // Skip ';' or '\n'
0909         }
0910     }
0911 
0912     return cookieList;
0913 }
0914 
0915 // KHttpCookieList sorting
0916 ///////////////////////////////////////////////////////////////////////////
0917 
0918 // We want the longest path first
0919 static bool compareCookies(const KHttpCookie &item1, const KHttpCookie &item2)
0920 {
0921     return item1.path().length() > item2.path().length();
0922 }
0923 
0924 #ifdef MAX_COOKIE_LIMIT
0925 static void makeRoom(KHttpCookieList *cookieList, KHttpCookiePtr &cookiePtr)
0926 {
0927     // Too many cookies: throw one away, try to be somewhat clever
0928     KHttpCookiePtr lastCookie = 0;
0929     for (KHttpCookiePtr cookie = cookieList->first(); cookie; cookie = cookieList->next()) {
0930         if (compareCookies(cookie, cookiePtr)) {
0931             break;
0932         }
0933         lastCookie = cookie;
0934     }
0935     if (!lastCookie) {
0936         lastCookie = cookieList->first();
0937     }
0938     cookieList->removeRef(lastCookie);
0939 }
0940 #endif
0941 
0942 //
0943 // This function hands a KHttpCookie object over to the cookie jar.
0944 //
0945 void KCookieJar::addCookie(KHttpCookie &cookie)
0946 {
0947     QStringList domains;
0948     // We always need to do this to make sure that the
0949     // that cookies of type hostname == cookie-domainname
0950     // are properly removed and/or updated as necessary!
0951     extractDomains(cookie.host(), domains);
0952 
0953     // If the cookie specifies a domain, check whether it is valid. Otherwise,
0954     // accept the cookie anyways but removes the domain="" value to prevent
0955     // cross-site cookie injection.
0956     if (!cookie.domain().isEmpty()) {
0957         if (!domains.contains(cookie.domain()) &&
0958                 !cookie.domain().endsWith(QL1C('.') + cookie.host())) {
0959             cookie.fixDomain(QString());
0960         }
0961     }
0962 
0963     QStringListIterator it(domains);
0964     while (it.hasNext()) {
0965         const QString &key = it.next();
0966         KHttpCookieList *list;
0967 
0968         if (key.isNull()) {
0969             list = m_cookieDomains.value(QL1S(""));
0970         } else {
0971             list = m_cookieDomains.value(key);
0972         }
0973 
0974         if (list) {
0975             removeDuplicateFromList(list, cookie, false, true);
0976         }
0977     }
0978 
0979     const QString domain = stripDomain(cookie);
0980     KHttpCookieList *cookieList;
0981     if (domain.isNull()) {
0982         cookieList = m_cookieDomains.value(QL1S(""));
0983     } else {
0984         cookieList = m_cookieDomains.value(domain);
0985     }
0986 
0987     if (!cookieList) {
0988         // Make a new cookie list
0989         cookieList = new KHttpCookieList();
0990 
0991         // All cookies whose domain is not already
0992         // known to us should be added with KCookieDunno.
0993         // KCookieDunno means that we use the global policy.
0994         cookieList->setAdvice(KCookieDunno);
0995 
0996         m_cookieDomains.insert(domain, cookieList);
0997 
0998         // Update the list of domains
0999         m_domainList.append(domain);
1000     }
1001 
1002     // Add the cookie to the cookie list
1003     // The cookie list is sorted 'longest path first'
1004     if (!cookie.isExpired()) {
1005 #ifdef MAX_COOKIE_LIMIT
1006         if (cookieList->count() >= MAX_COOKIES_PER_HOST) {
1007             makeRoom(cookieList, cookie);    // Delete a cookie
1008         }
1009 #endif
1010         cookieList->push_back(cookie);
1011         // Use a stable sort so that unit tests are reliable.
1012         // In practice it doesn't matter though.
1013         std::stable_sort(cookieList->begin(), cookieList->end(), compareCookies);
1014 
1015         m_cookiesChanged = true;
1016     }
1017 }
1018 
1019 //
1020 // This function advises whether a single KHttpCookie object should
1021 // be added to the cookie jar.
1022 //
1023 KCookieAdvice KCookieJar::cookieAdvice(const KHttpCookie &cookie) const
1024 {
1025     if (m_rejectCrossDomainCookies && cookie.isCrossDomain()) {
1026         return KCookieReject;
1027     }
1028 
1029     if (cookie.getUserSelectedAdvice() != KCookieDunno) {
1030         return cookie.getUserSelectedAdvice();
1031     }
1032 
1033     if (m_autoAcceptSessionCookies && cookie.expireDate() == 0) {
1034         return KCookieAccept;
1035     }
1036 
1037     QStringList domains;
1038     extractDomains(cookie.host(), domains);
1039 
1040     KCookieAdvice advice = KCookieDunno;
1041     QStringListIterator it(domains);
1042     while (advice == KCookieDunno && it.hasNext()) {
1043         const QString &domain = it.next();
1044         if (domain.startsWith(QL1C('.')) || cookie.host() == domain) {
1045             KHttpCookieList *cookieList = m_cookieDomains.value(domain);
1046             if (cookieList) {
1047                 advice = cookieList->getAdvice();
1048             }
1049         }
1050     }
1051 
1052     if (advice == KCookieDunno) {
1053         advice = m_globalAdvice;
1054     }
1055 
1056     return advice;
1057 }
1058 
1059 //
1060 // This function tells whether a single KHttpCookie object should
1061 // be considered persistent. Persistent cookies do not get deleted
1062 // at the end of the session and are saved on disk.
1063 //
1064 bool KCookieJar::cookieIsPersistent(const KHttpCookie &cookie) const
1065 {
1066     if (cookie.expireDate() == 0) {
1067         return false;
1068     }
1069 
1070     KCookieAdvice advice = cookieAdvice(cookie);
1071 
1072     if (advice == KCookieReject || advice == KCookieAcceptForSession) {
1073         return false;
1074     }
1075 
1076     return true;
1077 }
1078 
1079 //
1080 // This function gets the advice for all cookies originating from
1081 // _domain.
1082 //
1083 KCookieAdvice KCookieJar::getDomainAdvice(const QString &_domain) const
1084 {
1085     KHttpCookieList *cookieList = m_cookieDomains.value(_domain);
1086     KCookieAdvice advice;
1087 
1088     if (cookieList) {
1089         advice = cookieList->getAdvice();
1090     } else {
1091         advice = KCookieDunno;
1092     }
1093 
1094     return advice;
1095 }
1096 
1097 //
1098 // This function sets the advice for all cookies originating from
1099 // _domain.
1100 //
1101 void KCookieJar::setDomainAdvice(const QString &domain, KCookieAdvice _advice)
1102 {
1103     KHttpCookieList *cookieList = m_cookieDomains.value(domain);
1104 
1105     if (cookieList) {
1106         if (cookieList->getAdvice() != _advice) {
1107             m_configChanged = true;
1108             // domain is already known
1109             cookieList->setAdvice(_advice);
1110         }
1111 
1112         if ((cookieList->isEmpty()) && (_advice == KCookieDunno)) {
1113             // This deletes cookieList!
1114             delete m_cookieDomains.take(domain);
1115             m_domainList.removeAll(domain);
1116         }
1117     } else {
1118         // domain is not yet known
1119         if (_advice != KCookieDunno) {
1120             // We should create a domain entry
1121             m_configChanged = true;
1122             // Make a new cookie list
1123             cookieList = new KHttpCookieList();
1124             cookieList->setAdvice(_advice);
1125             m_cookieDomains.insert(domain, cookieList);
1126             // Update the list of domains
1127             m_domainList.append(domain);
1128         }
1129     }
1130 }
1131 
1132 //
1133 // This function sets the advice for all cookies originating from
1134 // the same domain as _cookie
1135 //
1136 void KCookieJar::setDomainAdvice(const KHttpCookie &cookie, KCookieAdvice _advice)
1137 {
1138     QString domain;
1139     stripDomain(cookie.host(), domain); // We file the cookie under this domain.
1140     setDomainAdvice(domain, _advice);
1141 }
1142 
1143 //
1144 // This function sets the global advice for cookies
1145 //
1146 void KCookieJar::setGlobalAdvice(KCookieAdvice _advice)
1147 {
1148     if (m_globalAdvice != _advice) {
1149         m_configChanged = true;
1150     }
1151     m_globalAdvice = _advice;
1152 }
1153 
1154 //
1155 // Get a list of all domains known to the cookie jar.
1156 //
1157 const QStringList &KCookieJar::getDomainList()
1158 {
1159     return m_domainList;
1160 }
1161 
1162 //
1163 // Get a list of all cookies in the cookie jar originating from _domain.
1164 //
1165 KHttpCookieList *KCookieJar::getCookieList(const QString &_domain,
1166         const QString &_fqdn)
1167 {
1168     QString domain;
1169 
1170     if (_domain.isEmpty()) {
1171         stripDomain(_fqdn, domain);
1172     } else {
1173         domain = _domain;
1174     }
1175 
1176     return m_cookieDomains.value(domain);
1177 }
1178 
1179 //
1180 // Eat a cookie out of the jar.
1181 // cookieIterator should be one of the cookies returned by getCookieList()
1182 //
1183 void KCookieJar::eatCookie(const KHttpCookieList::iterator &cookieIterator)
1184 {
1185     const KHttpCookie &cookie = *cookieIterator;
1186     const QString domain = stripDomain(cookie); // We file the cookie under this domain.
1187     KHttpCookieList *cookieList = m_cookieDomains.value(domain);
1188 
1189     if (cookieList) {
1190         // This deletes cookie!
1191         cookieList->erase(cookieIterator);
1192 
1193         if ((cookieList->isEmpty()) &&
1194                 (cookieList->getAdvice() == KCookieDunno)) {
1195             // This deletes cookieList!
1196             delete m_cookieDomains.take(domain);
1197             m_domainList.removeAll(domain);
1198         }
1199     }
1200 }
1201 
1202 void KCookieJar::eatCookiesForDomain(const QString &domain)
1203 {
1204     KHttpCookieList *cookieList = m_cookieDomains.value(domain);
1205     if (!cookieList || cookieList->isEmpty()) {
1206         return;
1207     }
1208 
1209     cookieList->clear();
1210     if (cookieList->getAdvice() == KCookieDunno) {
1211         // This deletes cookieList!
1212         delete m_cookieDomains.take(domain);
1213         m_domainList.removeAll(domain);
1214     }
1215     m_cookiesChanged = true;
1216 }
1217 
1218 void KCookieJar::eatSessionCookies(long windowId)
1219 {
1220     if (!windowId) {
1221         return;
1222     }
1223 
1224     for (const QString &domain : qAsConst(m_domainList)) {
1225         eatSessionCookies(domain, windowId, false);
1226     }
1227 }
1228 
1229 void KCookieJar::eatAllCookies()
1230 {
1231     // we need a copy as eatCookiesForDomain() might remove domain from m_domainList
1232     const QStringList list = m_domainList;
1233     for (const QString &domain : list) {
1234         eatCookiesForDomain(domain);
1235     }
1236 }
1237 
1238 void KCookieJar::eatSessionCookies(const QString &fqdn, WId windowId,
1239                                    bool isFQDN)
1240 {
1241     KHttpCookieList *cookieList;
1242     if (!isFQDN) {
1243         cookieList = m_cookieDomains.value(fqdn);
1244     } else {
1245         QString domain;
1246         stripDomain(fqdn, domain);
1247         cookieList = m_cookieDomains.value(domain);
1248     }
1249 
1250     if (cookieList) {
1251         QMutableListIterator<KHttpCookie> cookieIterator(*cookieList);
1252         while (cookieIterator.hasNext()) {
1253             KHttpCookie &cookie = cookieIterator.next();
1254 
1255             if (cookieIsPersistent(cookie)) {
1256                 continue;
1257             }
1258 
1259             QList<WId> &ids = cookie.windowIds();
1260 
1261 #ifndef NDEBUG
1262             if (ids.contains(windowId)) {
1263                 if (ids.count() > 1) {
1264                     qCDebug(KIO_COOKIEJAR) << "removing window id" << windowId << "from session cookie";
1265                 } else {
1266                     qCDebug(KIO_COOKIEJAR) << "deleting session cookie";
1267                 }
1268             }
1269 #endif
1270             if (!ids.removeAll(windowId) || !ids.isEmpty()) {
1271                 continue;
1272             }
1273             cookieIterator.remove();
1274         }
1275     }
1276 }
1277 
1278 static QString hostWithPort(const KHttpCookie *cookie)
1279 {
1280     const QList<int> &ports = cookie->ports();
1281 
1282     if (ports.isEmpty()) {
1283         return cookie->host();
1284     }
1285 
1286     QStringList portList;
1287     for (int port : ports) {
1288         portList << QString::number(port);
1289     }
1290 
1291     return (cookie->host() + QL1C(':') + portList.join(QLatin1Char(',')));
1292 }
1293 
1294 //
1295 // Saves all cookies to the file '_filename'.
1296 // On success 'true' is returned.
1297 // On failure 'false' is returned.
1298 bool KCookieJar::saveCookies(const QString &_filename)
1299 {
1300     QSaveFile cookieFile(_filename);
1301 
1302     if (!cookieFile.open(QIODevice::WriteOnly)) {
1303         return false;
1304     }
1305 
1306     QTextStream ts(&cookieFile);
1307 
1308     ts << "# KDE Cookie File v2\n#\n";
1309 
1310     const QString str =
1311         QString::asprintf("%-20s %-20s %-12s %-10s %-4s %-20s %-4s %s\n",
1312               "# Host", "Domain", "Path", "Exp.date", "Prot",
1313               "Name", "Sec", "Value");
1314     ts << str;
1315 
1316     QStringListIterator it(m_domainList);
1317     while (it.hasNext()) {
1318         const QString &domain = it.next();
1319         bool domainPrinted = false;
1320 
1321         KHttpCookieList *cookieList = m_cookieDomains.value(domain);
1322         if (!cookieList) {
1323             continue;
1324         }
1325 
1326         QMutableListIterator<KHttpCookie> cookieIterator(*cookieList);
1327         while (cookieIterator.hasNext()) {
1328             const KHttpCookie &cookie = cookieIterator.next();
1329 
1330             if (cookie.isExpired()) {
1331                 // Delete expired cookies
1332                 cookieIterator.remove();
1333                 continue;
1334             }
1335             if (cookieIsPersistent(cookie)) {
1336                 // Only save cookies that are not "session-only cookies"
1337                 if (!domainPrinted) {
1338                     domainPrinted = true;
1339                     ts << '[' << domain.toLocal8Bit().data() << "]\n";
1340                 }
1341                 // Store persistent cookies
1342                 const QString path = QL1C('"') + cookie.path() + QL1C('"');
1343                 const QString domain = QL1C('"') + cookie.domain() + QL1C('"');
1344                 const QString host = hostWithPort(&cookie);
1345 
1346                 // TODO: replace with direct QTextStream output ?
1347                 const QString str = QString::asprintf("%-20s %-20s %-12s %10lld  %3d %-20s %-4i %s\n",
1348                           host.toLatin1().constData(), domain.toLatin1().constData(),
1349                           path.toLatin1().constData(), cookie.expireDate(),
1350                           cookie.protocolVersion(),
1351                           cookie.name().isEmpty() ? cookie.value().toLatin1().constData() : cookie.name().toLatin1().constData(),
1352                           (cookie.isSecure() ? 1 : 0) + (cookie.isHttpOnly() ? 2 : 0) +
1353                           (cookie.hasExplicitPath() ? 4 : 0) + (cookie.name().isEmpty() ? 8 : 0),
1354                           cookie.value().toLatin1().constData());
1355                 ts << str.toLatin1();
1356             }
1357         }
1358     }
1359 
1360     if (cookieFile.commit()) {
1361         QFile::setPermissions(_filename, QFile::ReadUser | QFile::WriteUser);
1362         return true;
1363     }
1364     return false;
1365 }
1366 
1367 static const char *parseField(char *&buffer, bool keepQuotes = false)
1368 {
1369     char *result;
1370     if (!keepQuotes && (*buffer == '\"')) {
1371         // Find terminating "
1372         buffer++;
1373         result = buffer;
1374         while ((*buffer != '\"') && (*buffer)) {
1375             buffer++;
1376         }
1377     } else {
1378         // Find first white space
1379         result = buffer;
1380         while ((*buffer != ' ') && (*buffer != '\t') && (*buffer != '\n') && (*buffer)) {
1381             buffer++;
1382         }
1383     }
1384 
1385     if (!*buffer) {
1386         return result;    //
1387     }
1388     *buffer++ = '\0';
1389 
1390     // Skip white-space
1391     while ((*buffer == ' ') || (*buffer == '\t') || (*buffer == '\n')) {
1392         buffer++;
1393     }
1394 
1395     return result;
1396 }
1397 
1398 static QString extractHostAndPorts(const QString &str, QList<int> *ports = nullptr)
1399 {
1400     if (str.isEmpty()) {
1401         return str;
1402     }
1403 
1404     const int index = str.indexOf(QL1C(':'));
1405     if (index == -1) {
1406         return str;
1407     }
1408 
1409     const QString host = str.left(index);
1410     if (ports) {
1411         bool ok;
1412         const QStringList list = str.mid(index + 1).split(QL1C(','));
1413         for (const QString &portStr : list) {
1414             const int portNum = portStr.toInt(&ok);
1415             if (ok) {
1416                 ports->append(portNum);
1417             }
1418         }
1419     }
1420 
1421     return host;
1422 }
1423 
1424 //
1425 // Reloads all cookies from the file '_filename'.
1426 // On success 'true' is returned.
1427 // On failure 'false' is returned.
1428 bool KCookieJar::loadCookies(const QString &_filename)
1429 {
1430     QFile cookieFile(_filename);
1431 
1432     if (!cookieFile.open(QIODevice::ReadOnly)) {
1433         return false;
1434     }
1435 
1436     int version = 1;
1437     bool success = false;
1438     char *buffer = new char[READ_BUFFER_SIZE];
1439     qint64 len = cookieFile.readLine(buffer, READ_BUFFER_SIZE - 1);
1440 
1441     if (len != -1) {
1442         if (qstrcmp(buffer, "# KDE Cookie File\n") == 0) {
1443             success = true;
1444         } else if (qstrcmp(buffer, "# KDE Cookie File v") > 0) {
1445             bool ok = false;
1446             const int verNum = QByteArray(buffer + 19, len - 19).trimmed().toInt(&ok);
1447             if (ok) {
1448                 version = verNum;
1449                 success = true;
1450             }
1451         }
1452     }
1453 
1454     if (success) {
1455         const qint64 currentTime = epoch();
1456         QList<int> ports;
1457 
1458         while (cookieFile.readLine(buffer, READ_BUFFER_SIZE - 1) != -1) {
1459             char *line = buffer;
1460             // Skip lines which begin with '#' or '['
1461             if ((line[0] == '#') || (line[0] == '[')) {
1462                 continue;
1463             }
1464 
1465             const QString host = extractHostAndPorts(QL1S(parseField(line)), &ports);
1466             const QString domain = QL1S(parseField(line));
1467             if (host.isEmpty() && domain.isEmpty()) {
1468                 continue;
1469             }
1470             const QString path = QL1S(parseField(line));
1471             const QString expStr = QL1S(parseField(line));
1472             if (expStr.isEmpty()) {
1473                 continue;
1474             }
1475             const qint64 expDate = expStr.toLongLong();
1476             const QString verStr = QL1S(parseField(line));
1477             if (verStr.isEmpty()) {
1478                 continue;
1479             }
1480             int protVer  = verStr.toInt();
1481             QString name = QL1S(parseField(line));
1482             bool keepQuotes = false;
1483             bool secure = false;
1484             bool httpOnly = false;
1485             bool explicitPath = false;
1486             const char *value = nullptr;
1487             if ((version == 2) || (protVer >= 200)) {
1488                 if (protVer >= 200) {
1489                     protVer -= 200;
1490                 }
1491                 int i = atoi(parseField(line));
1492                 secure = i & 1;
1493                 httpOnly = i & 2;
1494                 explicitPath = i & 4;
1495                 if (i & 8) {
1496                     name = QLatin1String("");
1497                 }
1498                 line[strlen(line) - 1] = '\0'; // Strip LF.
1499                 value = line;
1500             } else {
1501                 if (protVer >= 100) {
1502                     protVer -= 100;
1503                     keepQuotes = true;
1504                 }
1505                 value = parseField(line, keepQuotes);
1506                 secure = QByteArray(parseField(line)).toShort();
1507             }
1508 
1509             // Expired or parse error
1510             if (!value || expDate == 0 || expDate < currentTime) {
1511                 continue;
1512             }
1513 
1514             KHttpCookie cookie(host, domain, path, name, QString::fromUtf8(value), expDate,
1515                                protVer, secure, httpOnly, explicitPath);
1516             if (!ports.isEmpty()) {
1517                 cookie.mPorts = ports;
1518             }
1519             addCookie(cookie);
1520         }
1521     }
1522 
1523     delete [] buffer;
1524     m_cookiesChanged = false;
1525     return success;
1526 }
1527 
1528 //
1529 // Save the cookie configuration
1530 //
1531 
1532 void KCookieJar::saveConfig(KConfig *_config)
1533 {
1534     if (!m_configChanged) {
1535         return;
1536     }
1537 
1538     KConfigGroup dlgGroup(_config, "Cookie Dialog");
1539     dlgGroup.writeEntry("PreferredPolicy", static_cast<int>(m_preferredPolicy));
1540     dlgGroup.writeEntry("ShowCookieDetails", m_showCookieDetails);
1541     KConfigGroup policyGroup(_config, "Cookie Policy");
1542     policyGroup.writeEntry("CookieGlobalAdvice", adviceToStr(m_globalAdvice));
1543 
1544     QStringList domainSettings;
1545     QStringListIterator it(m_domainList);
1546     while (it.hasNext()) {
1547         const QString &domain = it.next();
1548         KCookieAdvice advice = getDomainAdvice(domain);
1549         if (advice != KCookieDunno) {
1550             const QString value = domain + QL1C(':') + adviceToStr(advice);
1551             domainSettings.append(value);
1552         }
1553     }
1554     policyGroup.writeEntry("CookieDomainAdvice", domainSettings);
1555     _config->sync();
1556     m_configChanged = false;
1557 }
1558 
1559 //
1560 // Load the cookie configuration
1561 //
1562 
1563 void KCookieJar::loadConfig(KConfig *_config, bool reparse)
1564 {
1565     if (reparse) {
1566         _config->reparseConfiguration();
1567     }
1568 
1569     KConfigGroup dlgGroup(_config, "Cookie Dialog");
1570     m_showCookieDetails = dlgGroup.readEntry("ShowCookieDetails", false);
1571     m_preferredPolicy = static_cast<KCookieDefaultPolicy>(dlgGroup.readEntry("PreferredPolicy", 0));
1572 
1573     KConfigGroup policyGroup(_config, "Cookie Policy");
1574     const QStringList domainSettings = policyGroup.readEntry("CookieDomainAdvice", QStringList());
1575     // Warning: those default values are duplicated in the kcm (kio/kcookiespolicies.cpp)
1576     m_rejectCrossDomainCookies = policyGroup.readEntry("RejectCrossDomainCookies", true);
1577     m_autoAcceptSessionCookies = policyGroup.readEntry("AcceptSessionCookies", true);
1578     m_globalAdvice = strToAdvice(policyGroup.readEntry("CookieGlobalAdvice", QStringLiteral("Accept")));
1579 
1580     // Reset current domain settings first.
1581     // We need a copy as setDomainAdvice() may modify m_domainList
1582     const QStringList list = m_domainList;
1583     for (const QString &domain : list) {
1584         setDomainAdvice(domain, KCookieDunno);
1585     }
1586 
1587     // Now apply the domain settings read from config file...
1588     for (QStringList::ConstIterator it = domainSettings.constBegin(), itEnd = domainSettings.constEnd();
1589             it != itEnd; ++it) {
1590         const QString &value = *it;
1591         const int sepPos = value.lastIndexOf(QL1C(':'));
1592         if (sepPos <= 0) {
1593             continue;
1594         }
1595 
1596         const QString domain(value.left(sepPos));
1597         KCookieAdvice advice = strToAdvice(value.mid(sepPos + 1));
1598         setDomainAdvice(domain, advice);
1599     }
1600 }
1601 
1602 QDebug operator<<(QDebug dbg, const KHttpCookie &cookie)
1603 {
1604     dbg.nospace() << cookie.cookieStr(false);
1605     return dbg.space();
1606 }
1607 
1608 QDebug operator<<(QDebug dbg, const KHttpCookieList &list)
1609 {
1610     for (const KHttpCookie &cookie : list) {
1611         dbg << cookie;
1612     }
1613     return dbg;
1614 }