summaryrefslogtreecommitdiffstats
path: root/kioslave/http/kcookiejar/kcookiejar.cpp
diff options
context:
space:
mode:
authortoma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da>2009-11-25 17:56:58 +0000
committertoma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da>2009-11-25 17:56:58 +0000
commitce4a32fe52ef09d8f5ff1dd22c001110902b60a2 (patch)
tree5ac38a06f3dde268dc7927dc155896926aaf7012 /kioslave/http/kcookiejar/kcookiejar.cpp
downloadtdelibs-ce4a32fe52ef09d8f5ff1dd22c001110902b60a2.tar.gz
tdelibs-ce4a32fe52ef09d8f5ff1dd22c001110902b60a2.zip
Copy the KDE 3.5 branch to branches/trinity for new KDE 3.5 features.
BUG:215923 git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/kdelibs@1054174 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'kioslave/http/kcookiejar/kcookiejar.cpp')
-rw-r--r--kioslave/http/kcookiejar/kcookiejar.cpp1558
1 files changed, 1558 insertions, 0 deletions
diff --git a/kioslave/http/kcookiejar/kcookiejar.cpp b/kioslave/http/kcookiejar/kcookiejar.cpp
new file mode 100644
index 000000000..5b5f78f6b
--- /dev/null
+++ b/kioslave/http/kcookiejar/kcookiejar.cpp
@@ -0,0 +1,1558 @@
+/* This file is part of the KDE File Manager
+
+ Copyright (C) 1998-2000 Waldo Bastian (bastian@kde.org)
+ Copyright (C) 2000,2001 Dawit Alemayehu (adawit@kde.org)
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, and/or sell copies of the
+ Software, and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+//----------------------------------------------------------------------------
+//
+// KDE File Manager -- HTTP Cookies
+// $Id$
+
+//
+// The cookie protocol is a mess. RFC2109 is a joke since nobody seems to
+// use it. Apart from that it is badly written.
+// We try to implement Netscape Cookies and try to behave us according to
+// RFC2109 as much as we can.
+//
+// We assume cookies do not contain any spaces (Netscape spec.)
+// According to RFC2109 this is allowed though.
+//
+
+#include <config.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#ifdef HAVE_SYS_PARAM_H
+#include <sys/param.h>
+#endif
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+
+#ifdef USE_SOLARIS
+#include <strings.h>
+#endif
+
+#include <stdlib.h>
+
+//#include <netinet/in.h>
+//#include <arpa/inet.h>
+
+#include <qstring.h>
+#include <qstrlist.h>
+#include <qptrlist.h>
+#include <qptrdict.h>
+#include <qfile.h>
+#include <qdir.h>
+#include <qregexp.h>
+
+#include <kurl.h>
+#include <krfcdate.h>
+#include <kconfig.h>
+#include <ksavefile.h>
+#include <kdebug.h>
+
+#include "kcookiejar.h"
+
+
+// BR87227
+// Waba: Should the number of cookies be limited?
+// I am not convinced of the need of such limit
+// Mozilla seems to limit to 20 cookies / domain
+// but it is unclear which policy it uses to expire
+// cookies when it exceeds that amount
+#undef MAX_COOKIE_LIMIT
+
+#define MAX_COOKIES_PER_HOST 25
+#define READ_BUFFER_SIZE 8192
+#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]?)"
+
+// Note with respect to QString::fromLatin1( )
+// Cookies are stored as 8 bit data and passed to kio_http as
+// latin1 regardless of their actual encoding.
+
+// L1 is used to indicate latin1 constants
+#define L1(x) QString::fromLatin1(x)
+
+template class QPtrList<KHttpCookie>;
+template class QPtrDict<KHttpCookieList>;
+
+QString KCookieJar::adviceToStr(KCookieAdvice _advice)
+{
+ switch( _advice )
+ {
+ case KCookieAccept: return L1("Accept");
+ case KCookieReject: return L1("Reject");
+ case KCookieAsk: return L1("Ask");
+ default: return L1("Dunno");
+ }
+}
+
+KCookieAdvice KCookieJar::strToAdvice(const QString &_str)
+{
+ if (_str.isEmpty())
+ return KCookieDunno;
+
+ QCString advice = _str.lower().latin1();
+
+ if (advice == "accept")
+ return KCookieAccept;
+ else if (advice == "reject")
+ return KCookieReject;
+ else if (advice == "ask")
+ return KCookieAsk;
+
+ return KCookieDunno;
+}
+
+// KHttpCookie
+///////////////////////////////////////////////////////////////////////////
+
+//
+// Cookie constructor
+//
+KHttpCookie::KHttpCookie(const QString &_host,
+ const QString &_domain,
+ const QString &_path,
+ const QString &_name,
+ const QString &_value,
+ time_t _expireDate,
+ int _protocolVersion,
+ bool _secure,
+ bool _httpOnly,
+ bool _explicitPath) :
+ mHost(_host),
+ mDomain(_domain),
+ mPath(_path.isEmpty() ? QString::null : _path),
+ mName(_name),
+ mValue(_value),
+ mExpireDate(_expireDate),
+ mProtocolVersion(_protocolVersion),
+ mSecure(_secure),
+ mHttpOnly(_httpOnly),
+ mExplicitPath(_explicitPath)
+{
+}
+
+//
+// Checks if a cookie has been expired
+//
+bool KHttpCookie::isExpired(time_t currentDate)
+{
+ return (mExpireDate != 0) && (mExpireDate < currentDate);
+}
+
+//
+// Returns a string for a HTTP-header
+//
+QString KHttpCookie::cookieStr(bool useDOMFormat)
+{
+ QString result;
+
+ if (useDOMFormat || (mProtocolVersion == 0))
+ {
+ if ( !mName.isEmpty() )
+ result = mName + '=';
+ result += mValue;
+ }
+ else
+ {
+ result = mName + '=' + mValue;
+ if (mExplicitPath)
+ result += L1("; $Path=\"") + mPath + L1("\"");
+ if (!mDomain.isEmpty())
+ result += L1("; $Domain=\"") + mDomain + L1("\"");
+ }
+ return result;
+}
+
+//
+// Returns whether this cookie should be send to this location.
+bool KHttpCookie::match(const QString &fqdn, const QStringList &domains,
+ const QString &path)
+{
+ // Cookie domain match check
+ if (mDomain.isEmpty())
+ {
+ if (fqdn != mHost)
+ return false;
+ }
+ else if (!domains.contains(mDomain))
+ {
+ if (mDomain[0] == '.')
+ return false;
+
+ // Maybe the domain needs an extra dot.
+ QString domain = '.' + mDomain;
+ if ( !domains.contains( domain ) )
+ if ( fqdn != mDomain )
+ return false;
+ }
+
+ // Cookie path match check
+ if (mPath.isEmpty())
+ return true;
+
+ // According to the netscape spec both http://www.acme.com/foobar,
+ // http://www.acme.com/foo.bar and http://www.acme.com/foo/bar
+ // match http://www.acme.com/foo.
+ // We only match http://www.acme.com/foo/bar
+
+ if( path.startsWith(mPath) &&
+ (
+ (path.length() == mPath.length() ) || // Paths are exact match
+ (path[mPath.length()-1] == '/') || // mPath ended with a slash
+ (path[mPath.length()] == '/') // A slash follows.
+ ))
+ return true; // Path of URL starts with cookie-path
+
+ return false;
+}
+
+// KHttpCookieList
+///////////////////////////////////////////////////////////////////////////
+
+int KHttpCookieList::compareItems( void * item1, void * item2)
+{
+ int pathLen1 = ((KHttpCookie *)item1)->path().length();
+ int pathLen2 = ((KHttpCookie *)item2)->path().length();
+ if (pathLen1 > pathLen2)
+ return -1;
+ if (pathLen1 < pathLen2)
+ return 1;
+ return 0;
+}
+
+
+// KCookieJar
+///////////////////////////////////////////////////////////////////////////
+
+//
+// Constructs a new cookie jar
+//
+// One jar should be enough for all cookies.
+//
+KCookieJar::KCookieJar()
+{
+ m_cookieDomains.setAutoDelete( true );
+ m_globalAdvice = KCookieDunno;
+ m_configChanged = false;
+ m_cookiesChanged = false;
+
+ KConfig cfg("khtml/domain_info", true, false, "data");
+ QStringList countries = cfg.readListEntry("twoLevelTLD");
+ for(QStringList::ConstIterator it = countries.begin();
+ it != countries.end(); ++it)
+ {
+ m_twoLevelTLD.replace(*it, (int *) 1);
+ }
+}
+
+//
+// Destructs the cookie jar
+//
+// Poor little cookies, they will all be eaten by the cookie monster!
+//
+KCookieJar::~KCookieJar()
+{
+ // Not much to do here
+}
+
+static void removeDuplicateFromList(KHttpCookieList *list, KHttpCookie *cookiePtr, bool nameMatchOnly=false, bool updateWindowId=false)
+{
+ QString domain1 = cookiePtr->domain();
+ if (domain1.isEmpty())
+ domain1 = cookiePtr->host();
+
+ for ( KHttpCookiePtr cookie=list->first(); cookie != 0; )
+ {
+ QString domain2 = cookie->domain();
+ if (domain2.isEmpty())
+ domain2 = cookie->host();
+
+ if (
+ (cookiePtr->name() == cookie->name()) &&
+ (
+ nameMatchOnly ||
+ ( (domain1 == domain2) && (cookiePtr->path() == cookie->path()) )
+ )
+ )
+ {
+ if (updateWindowId)
+ {
+ for(QValueList<long>::ConstIterator it = cookie->windowIds().begin();
+ it != cookie->windowIds().end(); ++it)
+ {
+ long windowId = *it;
+ if (windowId && (cookiePtr->windowIds().find(windowId) == cookiePtr->windowIds().end()))
+ {
+ cookiePtr->windowIds().append(windowId);
+ }
+ }
+ }
+ KHttpCookiePtr old_cookie = cookie;
+ cookie = list->next();
+ list->removeRef( old_cookie );
+ break;
+ }
+ else
+ {
+ cookie = list->next();
+ }
+ }
+}
+
+
+//
+// Looks for cookies in the cookie jar which are appropriate for _url.
+// Returned is a string containing all appropriate cookies in a format
+// which can be added to a HTTP-header without any additional processing.
+//
+QString KCookieJar::findCookies(const QString &_url, bool useDOMFormat, long windowId, KHttpCookieList *pendingCookies)
+{
+ QString cookieStr;
+ QStringList domains;
+ QString fqdn;
+ QString path;
+ KHttpCookiePtr cookie;
+ KCookieAdvice advice = m_globalAdvice;
+
+ if (!parseURL(_url, fqdn, path))
+ return cookieStr;
+
+ bool secureRequest = (_url.find( L1("https://"), 0, false) == 0 ||
+ _url.find( L1("webdavs://"), 0, false) == 0);
+
+ // kdDebug(7104) << "findCookies: URL= " << _url << ", secure = " << secureRequest << endl;
+
+ extractDomains(fqdn, domains);
+
+ KHttpCookieList allCookies;
+
+ for(QStringList::ConstIterator it = domains.begin();
+ true;
+ ++it)
+ {
+ KHttpCookieList *cookieList;
+ if (it == domains.end())
+ {
+ cookieList = pendingCookies; // Add pending cookies
+ pendingCookies = 0;
+ if (!cookieList)
+ break;
+ }
+ else
+ {
+ QString key = (*it).isNull() ? L1("") : (*it);
+ cookieList = m_cookieDomains[key];
+ if (!cookieList)
+ continue; // No cookies for this domain
+ }
+
+ if (cookieList->getAdvice() != KCookieDunno)
+ advice = cookieList->getAdvice();
+
+ for ( cookie=cookieList->first(); cookie != 0; cookie=cookieList->next() )
+ {
+ // If the we are setup to automatically accept all session cookies and to
+ // treat all cookies as session cookies or the current cookie is a session
+ // cookie, then send the cookie back regardless of either policy.
+ if (advice == KCookieReject &&
+ !(m_autoAcceptSessionCookies &&
+ (m_ignoreCookieExpirationDate || cookie->expireDate() == 0)))
+ continue;
+
+ if (!cookie->match(fqdn, domains, path))
+ continue;
+
+ if( cookie->isSecure() && !secureRequest )
+ continue;
+
+ if( cookie->isHttpOnly() && useDOMFormat )
+ continue;
+
+ // Do not send expired cookies.
+ if ( cookie->isExpired (time(0)) )
+ {
+ // Note there is no need to actually delete the cookie here
+ // since the cookieserver will invoke ::saveCookieJar because
+ // of the state change below. This will then do the job of
+ // deleting the cookie for us.
+ m_cookiesChanged = true;
+ continue;
+ }
+
+ if (windowId && (cookie->windowIds().find(windowId) == cookie->windowIds().end()))
+ {
+ cookie->windowIds().append(windowId);
+ }
+
+ if (it == domains.end()) // Only needed when processing pending cookies
+ removeDuplicateFromList(&allCookies, cookie);
+
+ allCookies.append(cookie);
+ }
+ if (it == domains.end())
+ break; // Finished.
+ }
+
+ int cookieCount = 0;
+
+ int protVersion=0;
+ for ( cookie=allCookies.first(); cookie != 0; cookie=allCookies.next() )
+ {
+ if (cookie->protocolVersion() > protVersion)
+ protVersion = cookie->protocolVersion();
+ }
+
+ for ( cookie=allCookies.first(); cookie != 0; cookie=allCookies.next() )
+ {
+ if (useDOMFormat)
+ {
+ if (cookieCount > 0)
+ cookieStr += L1("; ");
+ cookieStr += cookie->cookieStr(true);
+ }
+ else
+ {
+ if (cookieCount == 0)
+ {
+ cookieStr += L1("Cookie: ");
+ if (protVersion > 0)
+ {
+ QString version;
+ version.sprintf("$Version=%d; ", protVersion); // Without quotes
+ cookieStr += version;
+ }
+ }
+ else
+ {
+ cookieStr += L1("; ");
+ }
+ cookieStr += cookie->cookieStr(false);
+ }
+ cookieCount++;
+ }
+
+ return cookieStr;
+}
+
+//
+// This function parses a string like 'my_name="my_value";' and returns
+// 'my_name' in Name and 'my_value' in Value.
+//
+// A pointer to the end of the parsed part is returned.
+// This pointer points either to:
+// '\0' - The end of the string has reached.
+// ';' - Another my_name="my_value" pair follows
+// ',' - Another cookie follows
+// '\n' - Another header follows
+static const char * parseNameValue(const char *header,
+ QString &Name,
+ QString &Value,
+ bool keepQuotes=false,
+ bool rfcQuotes=false)
+{
+ const char *s = header;
+ // Parse 'my_name' part
+ for(; (*s != '='); s++)
+ {
+ if ((*s=='\0') || (*s==';') || (*s=='\n'))
+ {
+ // No '=' sign -> use string as the value, name is empty
+ // (behavior found in Mozilla and IE)
+ Name = "";
+ Value = QString::fromLatin1(header);
+ Value.truncate( s - header );
+ Value = Value.stripWhiteSpace();
+ return (s);
+ }
+ }
+
+ Name = header;
+ Name.truncate( s - header );
+ Name = Name.stripWhiteSpace();
+
+ // *s == '='
+ s++;
+
+ // Skip any whitespace
+ for(; (*s == ' ') || (*s == '\t'); s++)
+ {
+ if ((*s=='\0') || (*s==';') || (*s=='\n'))
+ {
+ // End of Name
+ Value = "";
+ return (s);
+ }
+ }
+
+ if ((rfcQuotes || !keepQuotes) && (*s == '\"'))
+ {
+ // Parse '"my_value"' part (quoted value)
+ if (keepQuotes)
+ header = s++;
+ else
+ header = ++s; // skip "
+ for(;(*s != '\"');s++)
+ {
+ if ((*s=='\0') || (*s=='\n'))
+ {
+ // End of Name
+ Value = QString::fromLatin1(header);
+ Value.truncate(s - header);
+ return (s);
+ }
+ }
+ Value = QString::fromLatin1(header);
+ // *s == '\"';
+ if (keepQuotes)
+ Value.truncate( ++s - header );
+ else
+ Value.truncate( s++ - header );
+
+ // Skip any remaining garbage
+ for(;; s++)
+ {
+ if ((*s=='\0') || (*s==';') || (*s=='\n'))
+ break;
+ }
+ }
+ else
+ {
+ // Parse 'my_value' part (unquoted value)
+ header = s;
+ while ((*s != '\0') && (*s != ';') && (*s != '\n'))
+ s++;
+ // End of Name
+ Value = QString::fromLatin1(header);
+ Value.truncate( s - header );
+ Value = Value.stripWhiteSpace();
+ }
+ return (s);
+
+}
+
+void KCookieJar::stripDomain(const QString &_fqdn, QString &_domain)
+{
+ QStringList domains;
+ extractDomains(_fqdn, domains);
+ if (domains.count() > 3)
+ _domain = domains[3];
+ else
+ _domain = domains[0];
+}
+
+QString KCookieJar::stripDomain( KHttpCookiePtr cookiePtr)
+{
+ QString domain; // We file the cookie under this domain.
+ if (cookiePtr->domain().isEmpty())
+ stripDomain( cookiePtr->host(), domain);
+ else
+ stripDomain (cookiePtr->domain(), domain);
+ return domain;
+}
+
+bool KCookieJar::parseURL(const QString &_url,
+ QString &_fqdn,
+ QString &_path)
+{
+ KURL kurl(_url);
+ if (!kurl.isValid())
+ return false;
+
+ _fqdn = kurl.host().lower();
+ if (kurl.port())
+ {
+ if (((kurl.protocol() == L1("http")) && (kurl.port() != 80)) ||
+ ((kurl.protocol() == L1("https")) && (kurl.port() != 443)))
+ {
+ _fqdn = L1("%1:%2").arg(kurl.port()).arg(_fqdn);
+ }
+ }
+
+ // Cookie spoofing protection. Since there is no way a path separator
+ // or escape encoded character is allowed in the hostname according
+ // to RFC 2396, reject attempts to include such things there!
+ if(_fqdn.find('/') > -1 || _fqdn.find('%') > -1)
+ {
+ return false; // deny everything!!
+ }
+
+ _path = kurl.path();
+ if (_path.isEmpty())
+ _path = L1("/");
+
+ QRegExp exp(L1("[\\\\/]\\.\\.[\\\\/]"));
+ // Weird path, cookie stealing attempt?
+ if (exp.search(_path) != -1)
+ return false; // Deny everything!!
+
+ return true;
+}
+
+void KCookieJar::extractDomains(const QString &_fqdn,
+ QStringList &_domains)
+{
+ // Return numeric IPv6 addresses as is...
+ if (_fqdn[0] == '[')
+ {
+ _domains.append( _fqdn );
+ return;
+ }
+ // Return numeric IPv4 addresses as is...
+ if ((_fqdn[0] >= '0') && (_fqdn[0] <= '9'))
+ {
+ if (_fqdn.find(QRegExp(IP_ADDRESS_EXPRESSION)) > -1)
+ {
+ _domains.append( _fqdn );
+ return;
+ }
+ }
+
+ QStringList partList = QStringList::split('.', _fqdn, false);
+
+ if (partList.count())
+ partList.remove(partList.begin()); // Remove hostname
+
+ while(partList.count())
+ {
+
+ if (partList.count() == 1)
+ break; // We only have a TLD left.
+
+ if ((partList.count() == 2) && (m_twoLevelTLD[partList[1].lower()]))
+ {
+ // This domain uses two-level TLDs in the form xxxx.yy
+ break;
+ }
+
+ if ((partList.count() == 2) && (partList[1].length() == 2))
+ {
+ // If this is a TLD, we should stop. (e.g. co.uk)
+ // We assume this is a TLD if it ends with .xx.yy or .x.yy
+ if (partList[0].length() <= 2)
+ break; // This is a TLD.
+
+ // Catch some TLDs that we miss with the previous check
+ // e.g. com.au, org.uk, mil.co
+ QCString t = partList[0].lower().utf8();
+ if ((t == "com") || (t == "net") || (t == "org") || (t == "gov") || (t == "edu") || (t == "mil") || (t == "int"))
+ break;
+ }
+
+ QString domain = partList.join(L1("."));
+ _domains.append(domain);
+ _domains.append('.' + domain);
+ partList.remove(partList.begin()); // Remove part
+ }
+
+ // Always add the FQDN at the start of the list for
+ // hostname == cookie-domainname checks!
+ _domains.prepend( '.' + _fqdn );
+ _domains.prepend( _fqdn );
+}
+
+
+/*
+ Changes dates in from the following format
+
+ Wed Sep 12 07:00:00 2007 GMT
+ to
+ Wed Sep 12 2007 07:00:00 GMT
+
+ to allow KRFCDate::parseDate to properly parse expiration date formats
+ used in cookies by some servers such as amazon.com. See BR# 145244.
+*/
+static QString fixupDateTime(const QString& dt)
+{
+ const int index = dt.find(QRegExp("[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}"));
+
+ if (index > -1)
+ {
+ QStringList dateStrList = QStringList::split(' ', dt.mid(index));
+ if (dateStrList.count() > 1)
+ {
+ QString date = dateStrList[0];
+ dateStrList[0] = dateStrList[1];
+ dateStrList[1] = date;
+ date = dt;
+ return date.replace(index, date.length(), dateStrList.join(" "));
+ }
+ }
+
+ return dt;
+}
+
+//
+// This function parses cookie_headers and returns a linked list of
+// KHttpCookie objects for all cookies found in cookie_headers.
+// If no cookies could be found 0 is returned.
+//
+// cookie_headers should be a concatenation of all lines of a HTTP-header
+// which start with "Set-Cookie". The lines should be separated by '\n's.
+//
+KHttpCookieList KCookieJar::makeCookies(const QString &_url,
+ const QCString &cookie_headers,
+ long windowId)
+{
+ KHttpCookieList cookieList;
+ KHttpCookieList cookieList2;
+ KHttpCookiePtr lastCookie = 0;
+ const char *cookieStr = cookie_headers.data();
+ QString Name;
+ QString Value;
+ QString fqdn;
+ QString path;
+ bool crossDomain = false;
+
+ if (!parseURL(_url, fqdn, path))
+ {
+ // Error parsing _url
+ return KHttpCookieList();
+ }
+ QString defaultPath;
+ int i = path.findRev('/');
+ if (i > 0)
+ defaultPath = path.left(i);
+
+ // The hard stuff :)
+ for(;;)
+ {
+ // check for "Set-Cookie"
+ if (strncmp(cookieStr, "Cross-Domain\n", 13) == 0)
+ {
+ cookieStr += 13;
+ crossDomain = true;
+ }
+ else if (strncasecmp(cookieStr, "Set-Cookie:", 11) == 0)
+ {
+ cookieStr = parseNameValue(cookieStr+11, Name, Value, true);
+
+ // Host = FQDN
+ // Default domain = ""
+ // Default path according to rfc2109
+
+ KHttpCookie *cookie = new KHttpCookie(fqdn, L1(""), defaultPath, Name, Value);
+ if (windowId)
+ cookie->mWindowIds.append(windowId);
+ cookie->mCrossDomain = crossDomain;
+
+ // Insert cookie in chain
+ cookieList.append(cookie);
+ lastCookie = cookie;
+ }
+ else if (strncasecmp(cookieStr, "Set-Cookie2:", 12) == 0)
+ {
+ // Attempt to follow rfc2965
+ cookieStr = parseNameValue(cookieStr+12, Name, Value, true, true);
+
+ // Host = FQDN
+ // Default domain = ""
+ // Default path according to rfc2965
+
+ KHttpCookie *cookie = new KHttpCookie(fqdn, L1(""), defaultPath, Name, Value);
+ if (windowId)
+ cookie->mWindowIds.append(windowId);
+ cookie->mCrossDomain = crossDomain;
+
+ // Insert cookie in chain
+ cookieList2.append(cookie);
+ lastCookie = cookie;
+ }
+ else
+ {
+ // This is not the start of a cookie header, skip till next line.
+ while (*cookieStr && *cookieStr != '\n')
+ cookieStr++;
+
+ if (*cookieStr == '\n')
+ cookieStr++;
+
+ if (!*cookieStr)
+ break; // End of cookie_headers
+ else
+ continue; // end of this header, continue with next.
+ }
+
+ while ((*cookieStr == ';') || (*cookieStr == ' '))
+ {
+ cookieStr++;
+
+ // Name-Value pair follows
+ cookieStr = parseNameValue(cookieStr, Name, Value);
+
+ QCString cName = Name.lower().latin1();
+ if (cName == "domain")
+ {
+ QString dom = Value.lower();
+ // RFC2965 3.2.2: If an explicitly specified value does not
+ // start with a dot, the user agent supplies a leading dot
+ if(dom.length() && dom[0] != '.')
+ dom.prepend(".");
+ // remove a trailing dot
+ if(dom.length() > 2 && dom[dom.length()-1] == '.')
+ dom = dom.left(dom.length()-1);
+
+ if(dom.contains('.') > 1 || dom == ".local")
+ lastCookie->mDomain = dom;
+ }
+ else if (cName == "max-age")
+ {
+ int max_age = Value.toInt();
+ if (max_age == 0)
+ lastCookie->mExpireDate = 1;
+ else
+ lastCookie->mExpireDate = time(0)+max_age;
+ }
+ else if (cName == "expires")
+ {
+ // Parse brain-dead netscape cookie-format
+ lastCookie->mExpireDate = KRFCDate::parseDate(Value);
+
+ // Workaround for servers that send the expiration date in
+ // 'Wed Sep 12 07:00:00 2007 GMT' format. See BR# 145244.
+ if (lastCookie->mExpireDate == 0)
+ lastCookie->mExpireDate = KRFCDate::parseDate(fixupDateTime(Value));
+ }
+ else if (cName == "path")
+ {
+ if (Value.isEmpty())
+ lastCookie->mPath = QString::null; // Catch "" <> QString::null
+ else
+ lastCookie->mPath = KURL::decode_string(Value);
+ lastCookie->mExplicitPath = true;
+ }
+ else if (cName == "version")
+ {
+ lastCookie->mProtocolVersion = Value.toInt();
+ }
+ else if ((cName == "secure") ||
+ (cName.isEmpty() && Value.lower() == L1("secure")))
+ {
+ lastCookie->mSecure = true;
+ }
+ else if ((cName == "httponly") ||
+ (cName.isEmpty() && Value.lower() == L1("httponly")))
+ {
+ lastCookie->mHttpOnly = true;
+ }
+ }
+
+ if (*cookieStr == '\0')
+ break; // End of header
+
+ // Skip ';' or '\n'
+ cookieStr++;
+ }
+
+ // RFC2965 cookies come last so that they override netscape cookies.
+ while( !cookieList2.isEmpty() && (lastCookie = cookieList2.take(0)) )
+ {
+ removeDuplicateFromList(&cookieList, lastCookie, true);
+ cookieList.append(lastCookie);
+ }
+
+ return cookieList;
+}
+
+/**
+* Parses cookie_domstr and returns a linked list of KHttpCookie objects.
+* cookie_domstr should be a semicolon-delimited list of "name=value"
+* pairs. Any whitespace before "name" or around '=' is discarded.
+* If no cookies are found, 0 is returned.
+*/
+KHttpCookieList KCookieJar::makeDOMCookies(const QString &_url,
+ const QCString &cookie_domstring,
+ long windowId)
+{
+ // A lot copied from above
+ KHttpCookieList cookieList;
+ KHttpCookiePtr lastCookie = 0;
+
+ const char *cookieStr = cookie_domstring.data();
+ QString Name;
+ QString Value;
+ QString fqdn;
+ QString path;
+
+ if (!parseURL(_url, fqdn, path))
+ {
+ // Error parsing _url
+ return KHttpCookieList();
+ }
+
+ // This time it's easy
+ while(*cookieStr)
+ {
+ cookieStr = parseNameValue(cookieStr, Name, Value);
+
+ // Host = FQDN
+ // Default domain = ""
+ // Default path = ""
+ KHttpCookie *cookie = new KHttpCookie(fqdn, QString::null, QString::null,
+ Name, Value );
+ if (windowId)
+ cookie->mWindowIds.append(windowId);
+
+ cookieList.append(cookie);
+ lastCookie = cookie;
+
+ if (*cookieStr != '\0')
+ cookieStr++; // Skip ';' or '\n'
+ }
+
+ return cookieList;
+}
+
+#ifdef MAX_COOKIE_LIMIT
+static void makeRoom(KHttpCookieList *cookieList, KHttpCookiePtr &cookiePtr)
+{
+ // Too much cookies: throw one away, try to be somewhat clever
+ KHttpCookiePtr lastCookie = 0;
+ for(KHttpCookiePtr cookie = cookieList->first(); cookie; cookie = cookieList->next())
+ {
+ if (cookieList->compareItems(cookie, cookiePtr) < 0)
+ break;
+ lastCookie = cookie;
+ }
+ if (!lastCookie)
+ lastCookie = cookieList->first();
+ cookieList->removeRef(lastCookie);
+}
+#endif
+
+//
+// This function hands a KHttpCookie object over to the cookie jar.
+//
+// On return cookiePtr is set to 0.
+//
+void KCookieJar::addCookie(KHttpCookiePtr &cookiePtr)
+{
+ QStringList domains;
+ KHttpCookieList *cookieList = 0L;
+
+ // We always need to do this to make sure that the
+ // that cookies of type hostname == cookie-domainname
+ // are properly removed and/or updated as necessary!
+ extractDomains( cookiePtr->host(), domains );
+ for ( QStringList::ConstIterator it = domains.begin();
+ (it != domains.end() && !cookieList);
+ ++it )
+ {
+ QString key = (*it).isNull() ? L1("") : (*it);
+ KHttpCookieList *list= m_cookieDomains[key];
+ if ( !list ) continue;
+
+ removeDuplicateFromList(list, cookiePtr, false, true);
+ }
+
+ QString domain = stripDomain( cookiePtr );
+ QString key = domain.isNull() ? L1("") : domain;
+ cookieList = m_cookieDomains[ key ];
+ if (!cookieList)
+ {
+ // Make a new cookie list
+ cookieList = new KHttpCookieList();
+ cookieList->setAutoDelete(true);
+
+ // All cookies whose domain is not already
+ // known to us should be added with KCookieDunno.
+ // KCookieDunno means that we use the global policy.
+ cookieList->setAdvice( KCookieDunno );
+
+ m_cookieDomains.insert( domain, cookieList);
+
+ // Update the list of domains
+ m_domainList.append(domain);
+ }
+
+ // Add the cookie to the cookie list
+ // The cookie list is sorted 'longest path first'
+ if (!cookiePtr->isExpired(time(0)))
+ {
+#ifdef MAX_COOKIE_LIMIT
+ if (cookieList->count() >= MAX_COOKIES_PER_HOST)
+ makeRoom(cookieList, cookiePtr); // Delete a cookie
+#endif
+ cookieList->inSort( cookiePtr );
+ m_cookiesChanged = true;
+ }
+ else
+ {
+ delete cookiePtr;
+ }
+ cookiePtr = 0;
+}
+
+//
+// This function advices whether a single KHttpCookie object should
+// be added to the cookie jar.
+//
+KCookieAdvice KCookieJar::cookieAdvice(KHttpCookiePtr cookiePtr)
+{
+ if (m_rejectCrossDomainCookies && cookiePtr->isCrossDomain())
+ return KCookieReject;
+
+ QStringList domains;
+
+ extractDomains(cookiePtr->host(), domains);
+
+ // If the cookie specifies a domain, check whether it is valid. Otherwise,
+ // accept the cookie anyways but remove the domain="" value to prevent
+ // cross-site cookie injection.
+ if (!cookiePtr->domain().isEmpty())
+ {
+ if (!domains.contains(cookiePtr->domain()) &&
+ !cookiePtr->domain().endsWith("."+cookiePtr->host()))
+ cookiePtr->fixDomain(QString::null);
+ }
+
+ if (m_autoAcceptSessionCookies && (cookiePtr->expireDate() == 0 ||
+ m_ignoreCookieExpirationDate))
+ return KCookieAccept;
+
+ KCookieAdvice advice = KCookieDunno;
+ bool isFQDN = true; // First is FQDN
+ QStringList::Iterator it = domains.begin(); // Start with FQDN which first in the list.
+ while( (advice == KCookieDunno) && (it != domains.end()))
+ {
+ QString domain = *it;
+ // Check if a policy for the FQDN/domain is set.
+ if ( domain[0] == '.' || isFQDN )
+ {
+ isFQDN = false;
+ KHttpCookieList *cookieList = m_cookieDomains[domain];
+ if (cookieList)
+ advice = cookieList->getAdvice();
+ }
+ domains.remove(it);
+ it = domains.begin(); // Continue from begin of remaining list
+ }
+
+ if (advice == KCookieDunno)
+ advice = m_globalAdvice;
+
+ return advice;
+}
+
+//
+// This function gets the advice for all cookies originating from
+// _domain.
+//
+KCookieAdvice KCookieJar::getDomainAdvice(const QString &_domain)
+{
+ KHttpCookieList *cookieList = m_cookieDomains[_domain];
+ KCookieAdvice advice;
+
+ if (cookieList)
+ {
+ advice = cookieList->getAdvice();
+ }
+ else
+ {
+ advice = KCookieDunno;
+ }
+
+ return advice;
+}
+
+//
+// This function sets the advice for all cookies originating from
+// _domain.
+//
+void KCookieJar::setDomainAdvice(const QString &_domain, KCookieAdvice _advice)
+{
+ QString domain(_domain);
+ KHttpCookieList *cookieList = m_cookieDomains[domain];
+
+ if (cookieList)
+ {
+ if (cookieList->getAdvice() != _advice)
+ {
+ m_configChanged = true;
+ // domain is already known
+ cookieList->setAdvice( _advice);
+ }
+
+ if ((cookieList->isEmpty()) &&
+ (_advice == KCookieDunno))
+ {
+ // This deletes cookieList!
+ m_cookieDomains.remove(domain);
+ m_domainList.remove(domain);
+ }
+ }
+ else
+ {
+ // domain is not yet known
+ if (_advice != KCookieDunno)
+ {
+ // We should create a domain entry
+ m_configChanged = true;
+ // Make a new cookie list
+ cookieList = new KHttpCookieList();
+ cookieList->setAutoDelete(true);
+ cookieList->setAdvice( _advice);
+ m_cookieDomains.insert( domain, cookieList);
+ // Update the list of domains
+ m_domainList.append( domain);
+ }
+ }
+}
+
+//
+// This function sets the advice for all cookies originating from
+// the same domain as _cookie
+//
+void KCookieJar::setDomainAdvice(KHttpCookiePtr cookiePtr, KCookieAdvice _advice)
+{
+ QString domain;
+ stripDomain(cookiePtr->host(), domain); // We file the cookie under this domain.
+
+ setDomainAdvice(domain, _advice);
+}
+
+//
+// This function sets the global advice for cookies
+//
+void KCookieJar::setGlobalAdvice(KCookieAdvice _advice)
+{
+ if (m_globalAdvice != _advice)
+ m_configChanged = true;
+ m_globalAdvice = _advice;
+}
+
+//
+// Get a list of all domains known to the cookie jar.
+//
+const QStringList& KCookieJar::getDomainList()
+{
+ return m_domainList;
+}
+
+//
+// Get a list of all cookies in the cookie jar originating from _domain.
+//
+const KHttpCookieList *KCookieJar::getCookieList(const QString & _domain,
+ const QString & _fqdn )
+{
+ QString domain;
+
+ if (_domain.isEmpty())
+ stripDomain( _fqdn, domain );
+ else
+ domain = _domain;
+
+ return m_cookieDomains[domain];
+}
+
+//
+// Eat a cookie out of the jar.
+// cookiePtr should be one of the cookies returned by getCookieList()
+//
+void KCookieJar::eatCookie(KHttpCookiePtr cookiePtr)
+{
+ QString domain = stripDomain(cookiePtr); // We file the cookie under this domain.
+ KHttpCookieList *cookieList = m_cookieDomains[domain];
+
+ if (cookieList)
+ {
+ // This deletes cookiePtr!
+ if (cookieList->removeRef( cookiePtr ))
+ m_cookiesChanged = true;
+
+ if ((cookieList->isEmpty()) &&
+ (cookieList->getAdvice() == KCookieDunno))
+ {
+ // This deletes cookieList!
+ m_cookieDomains.remove(domain);
+
+ m_domainList.remove(domain);
+ }
+ }
+}
+
+void KCookieJar::eatCookiesForDomain(const QString &domain)
+{
+ KHttpCookieList *cookieList = m_cookieDomains[domain];
+ if (!cookieList || cookieList->isEmpty()) return;
+
+ cookieList->clear();
+ if (cookieList->getAdvice() == KCookieDunno)
+ {
+ // This deletes cookieList!
+ m_cookieDomains.remove(domain);
+ m_domainList.remove(domain);
+ }
+ m_cookiesChanged = true;
+}
+
+void KCookieJar::eatSessionCookies( long windowId )
+{
+ if (!windowId)
+ return;
+
+ QStringList::Iterator it=m_domainList.begin();
+ for ( ; it != m_domainList.end(); ++it )
+ eatSessionCookies( *it, windowId, false );
+}
+
+void KCookieJar::eatAllCookies()
+{
+ for ( QStringList::Iterator it=m_domainList.begin();
+ it != m_domainList.end();)
+ {
+ QString domain = *it++;
+ // This might remove domain from domainList!
+ eatCookiesForDomain(domain);
+ }
+}
+
+void KCookieJar::eatSessionCookies( const QString& fqdn, long windowId,
+ bool isFQDN )
+{
+ KHttpCookieList* cookieList;
+ if ( !isFQDN )
+ cookieList = m_cookieDomains[fqdn];
+ else
+ {
+ QString domain;
+ stripDomain( fqdn, domain );
+ cookieList = m_cookieDomains[domain];
+ }
+
+ if ( cookieList )
+ {
+ KHttpCookiePtr cookie=cookieList->first();
+ for (; cookie != 0;)
+ {
+ if ((cookie->expireDate() != 0) && !m_ignoreCookieExpirationDate)
+ {
+ cookie = cookieList->next();
+ continue;
+ }
+
+ QValueList<long> &ids = cookie->windowIds();
+ if (!ids.remove(windowId) || !ids.isEmpty())
+ {
+ cookie = cookieList->next();
+ continue;
+ }
+ KHttpCookiePtr old_cookie = cookie;
+ cookie = cookieList->next();
+ cookieList->removeRef( old_cookie );
+ }
+ }
+}
+
+//
+// Saves all cookies to the file '_filename'.
+// On succes 'true' is returned.
+// On failure 'false' is returned.
+bool KCookieJar::saveCookies(const QString &_filename)
+{
+ KSaveFile saveFile(_filename, 0600);
+
+ if (saveFile.status() != 0)
+ return false;
+
+ FILE *fStream = saveFile.fstream();
+
+ time_t curTime = time(0);
+
+ fprintf(fStream, "# KDE Cookie File v2\n#\n");
+
+ fprintf(fStream, "%-20s %-20s %-12s %-10s %-4s %-20s %-4s %s\n",
+ "# Host", "Domain", "Path", "Exp.date", "Prot",
+ "Name", "Sec", "Value");
+
+ for ( QStringList::Iterator it=m_domainList.begin(); it != m_domainList.end();
+ it++ )
+ {
+ const QString &domain = *it;
+ bool domainPrinted = false;
+
+ KHttpCookieList *cookieList = m_cookieDomains[domain];
+ KHttpCookiePtr cookie=cookieList->last();
+
+ for (; cookie != 0;)
+ {
+ if (cookie->isExpired(curTime))
+ {
+ // Delete expired cookies
+ KHttpCookiePtr old_cookie = cookie;
+ cookie = cookieList->prev();
+ cookieList->removeRef( old_cookie );
+ }
+ else if (cookie->expireDate() != 0 && !m_ignoreCookieExpirationDate)
+ {
+ if (!domainPrinted)
+ {
+ domainPrinted = true;
+ fprintf(fStream, "[%s]\n", domain.local8Bit().data());
+ }
+ // Store persistent cookies
+ QString path = L1("\"");
+ path += cookie->path();
+ path += '"';
+ QString domain = L1("\"");
+ domain += cookie->domain();
+ domain += '"';
+ fprintf(fStream, "%-20s %-20s %-12s %10lu %3d %-20s %-4i %s\n",
+ cookie->host().latin1(), domain.latin1(),
+ path.latin1(), (unsigned long) cookie->expireDate(),
+ cookie->protocolVersion(),
+ cookie->name().isEmpty() ? cookie->value().latin1() : cookie->name().latin1(),
+ (cookie->isSecure() ? 1 : 0) + (cookie->isHttpOnly() ? 2 : 0) +
+ (cookie->hasExplicitPath() ? 4 : 0) + (cookie->name().isEmpty() ? 8 : 0),
+ cookie->value().latin1());
+ cookie = cookieList->prev();
+ }
+ else
+ {
+ // Skip session-only cookies
+ cookie = cookieList->prev();
+ }
+ }
+ }
+
+ return saveFile.close();
+}
+
+typedef char *charPtr;
+
+static const char *parseField(charPtr &buffer, bool keepQuotes=false)
+{
+ char *result;
+ if (!keepQuotes && (*buffer == '\"'))
+ {
+ // Find terminating "
+ buffer++;
+ result = buffer;
+ while((*buffer != '\"') && (*buffer))
+ buffer++;
+ }
+ else
+ {
+ // Find first white space
+ result = buffer;
+ while((*buffer != ' ') && (*buffer != '\t') && (*buffer != '\n') && (*buffer))
+ buffer++;
+ }
+
+ if (!*buffer)
+ return result; //
+ *buffer++ = '\0';
+
+ // Skip white-space
+ while((*buffer == ' ') || (*buffer == '\t') || (*buffer == '\n'))
+ buffer++;
+
+ return result;
+}
+
+
+//
+// Reloads all cookies from the file '_filename'.
+// On succes 'true' is returned.
+// On failure 'false' is returned.
+bool KCookieJar::loadCookies(const QString &_filename)
+{
+ FILE *fStream = fopen( QFile::encodeName(_filename), "r");
+ if (fStream == 0)
+ {
+ return false;
+ }
+
+ time_t curTime = time(0);
+
+ char *buffer = new char[READ_BUFFER_SIZE];
+
+ bool err = false;
+ err = (fgets(buffer, READ_BUFFER_SIZE, fStream) == 0);
+
+ int version = 1;
+ if (!err)
+ {
+ if (strcmp(buffer, "# KDE Cookie File\n") == 0)
+ {
+ // version 1
+ }
+ else if (sscanf(buffer, "# KDE Cookie File v%d\n", &version) != 1)
+ {
+ err = true;
+ }
+ }
+
+ if (!err)
+ {
+ while(fgets(buffer, READ_BUFFER_SIZE, fStream) != 0)
+ {
+ char *line = buffer;
+ // Skip lines which begin with '#' or '['
+ if ((line[0] == '#') || (line[0] == '['))
+ continue;
+
+ const char *host( parseField(line) );
+ const char *domain( parseField(line) );
+ const char *path( parseField(line) );
+ const char *expStr( parseField(line) );
+ if (!expStr) continue;
+ int expDate = (time_t) strtoul(expStr, 0, 10);
+ const char *verStr( parseField(line) );
+ if (!verStr) continue;
+ int protVer = (time_t) strtoul(verStr, 0, 10);
+ const char *name( parseField(line) );
+ bool keepQuotes = false;
+ bool secure = false;
+ bool httpOnly = false;
+ bool explicitPath = false;
+ const char *value = 0;
+ if ((version == 2) || (protVer >= 200))
+ {
+ if (protVer >= 200)
+ protVer -= 200;
+ int i = atoi( parseField(line) );
+ secure = i & 1;
+ httpOnly = i & 2;
+ explicitPath = i & 4;
+ if (i & 8)
+ name = "";
+ line[strlen(line)-1] = '\0'; // Strip LF.
+ value = line;
+ }
+ else
+ {
+ if (protVer >= 100)
+ {
+ protVer -= 100;
+ keepQuotes = true;
+ }
+ value = parseField(line, keepQuotes);
+ secure = atoi( parseField(line) );
+ }
+
+ // Parse error
+ if (!value) continue;
+
+ // Expired or parse error
+ if ((expDate == 0) || (expDate < curTime))
+ continue;
+
+ KHttpCookie *cookie = new KHttpCookie(QString::fromLatin1(host),
+ QString::fromLatin1(domain),
+ QString::fromLatin1(path),
+ QString::fromLatin1(name),
+ QString::fromLatin1(value),
+ expDate, protVer,
+ secure, httpOnly, explicitPath);
+ addCookie(cookie);
+ }
+ }
+ delete [] buffer;
+ m_cookiesChanged = false;
+
+ fclose( fStream);
+ return err;
+}
+
+//
+// Save the cookie configuration
+//
+
+void KCookieJar::saveConfig(KConfig *_config)
+{
+ if (!m_configChanged)
+ return;
+
+ _config->setGroup("Cookie Dialog");
+ _config->writeEntry("PreferredPolicy", m_preferredPolicy);
+ _config->writeEntry("ShowCookieDetails", m_showCookieDetails );
+ _config->setGroup("Cookie Policy");
+ _config->writeEntry("CookieGlobalAdvice", adviceToStr( m_globalAdvice));
+
+ QStringList domainSettings;
+ for ( QStringList::Iterator it=m_domainList.begin();
+ it != m_domainList.end();
+ it++ )
+ {
+ const QString &domain = *it;
+ KCookieAdvice advice = getDomainAdvice( domain);
+ if (advice != KCookieDunno)
+ {
+ QString value(domain);
+ value += ':';
+ value += adviceToStr(advice);
+ domainSettings.append(value);
+ }
+ }
+ _config->writeEntry("CookieDomainAdvice", domainSettings);
+ _config->sync();
+ m_configChanged = false;
+}
+
+
+//
+// Load the cookie configuration
+//
+
+void KCookieJar::loadConfig(KConfig *_config, bool reparse )
+{
+ if ( reparse )
+ _config->reparseConfiguration();
+
+ _config->setGroup("Cookie Dialog");
+ m_showCookieDetails = _config->readBoolEntry( "ShowCookieDetails" );
+ m_preferredPolicy = _config->readNumEntry( "PreferredPolicy", 0 );
+
+ _config->setGroup("Cookie Policy");
+ QStringList domainSettings = _config->readListEntry("CookieDomainAdvice");
+ m_rejectCrossDomainCookies = _config->readBoolEntry( "RejectCrossDomainCookies", true );
+ m_autoAcceptSessionCookies = _config->readBoolEntry( "AcceptSessionCookies", true );
+ m_ignoreCookieExpirationDate = _config->readBoolEntry( "IgnoreExpirationDate", false );
+ QString value = _config->readEntry("CookieGlobalAdvice", L1("Ask"));
+ m_globalAdvice = strToAdvice(value);
+
+ // Reset current domain settings first.
+ for ( QStringList::Iterator it=m_domainList.begin(); it != m_domainList.end(); )
+ {
+ // Make sure to update iterator before calling setDomainAdvice()
+ // setDomainAdvice() might delete the domain from domainList.
+ QString domain = *it++;
+ setDomainAdvice(domain, KCookieDunno);
+ }
+
+ // Now apply the domain settings read from config file...
+ for ( QStringList::Iterator it=domainSettings.begin();
+ it != domainSettings.end(); )
+ {
+ const QString &value = *it++;
+
+ int sepPos = value.findRev(':');
+
+ if (sepPos <= 0)
+ continue;
+
+ QString domain(value.left(sepPos));
+ KCookieAdvice advice = strToAdvice( value.mid(sepPos + 1) );
+ setDomainAdvice(domain, advice);
+ }
+}