1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.webkit;
18
19import android.net.ParseException;
20import android.net.WebAddress;
21import android.net.http.AndroidHttpClient;
22import android.os.AsyncTask;
23import android.util.Log;
24
25
26import java.util.ArrayList;
27import java.util.Arrays;
28import java.util.Collection;
29import java.util.Comparator;
30import java.util.Iterator;
31import java.util.LinkedHashMap;
32import java.util.Map;
33import java.util.SortedSet;
34import java.util.TreeSet;
35
36/**
37 * CookieManager manages cookies according to RFC2109 spec.
38 */
39public final class CookieManager {
40
41    private static CookieManager sRef;
42
43    private static final String LOGTAG = "webkit";
44
45    private static final String DOMAIN = "domain";
46
47    private static final String PATH = "path";
48
49    private static final String EXPIRES = "expires";
50
51    private static final String SECURE = "secure";
52
53    private static final String MAX_AGE = "max-age";
54
55    private static final String HTTP_ONLY = "httponly";
56
57    private static final String HTTPS = "https";
58
59    private static final char PERIOD = '.';
60
61    private static final char COMMA = ',';
62
63    private static final char SEMICOLON = ';';
64
65    private static final char EQUAL = '=';
66
67    private static final char PATH_DELIM = '/';
68
69    private static final char QUESTION_MARK = '?';
70
71    private static final char WHITE_SPACE = ' ';
72
73    private static final char QUOTATION = '\"';
74
75    private static final int SECURE_LENGTH = SECURE.length();
76
77    private static final int HTTP_ONLY_LENGTH = HTTP_ONLY.length();
78
79    // RFC2109 defines 4k as maximum size of a cookie
80    private static final int MAX_COOKIE_LENGTH = 4 * 1024;
81
82    // RFC2109 defines 20 as max cookie count per domain. As we track with base
83    // domain, we allow 50 per base domain
84    private static final int MAX_COOKIE_COUNT_PER_BASE_DOMAIN = 50;
85
86    // RFC2109 defines 300 as max count of domains. As we track with base
87    // domain, we set 200 as max base domain count
88    private static final int MAX_DOMAIN_COUNT = 200;
89
90    // max cookie count to limit RAM cookie takes less than 100k, it is based on
91    // average cookie entry size is less than 100 bytes
92    private static final int MAX_RAM_COOKIES_COUNT = 1000;
93
94    //  max domain count to limit RAM cookie takes less than 100k,
95    private static final int MAX_RAM_DOMAIN_COUNT = 15;
96
97    private Map<String, ArrayList<Cookie>> mCookieMap = new LinkedHashMap
98            <String, ArrayList<Cookie>>(MAX_DOMAIN_COUNT, 0.75f, true);
99
100    private boolean mAcceptCookie = true;
101
102    private int pendingCookieOperations = 0;
103
104    /**
105     * This contains a list of 2nd-level domains that aren't allowed to have
106     * wildcards when combined with country-codes. For example: [.co.uk].
107     */
108    private final static String[] BAD_COUNTRY_2LDS =
109          { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
110            "lg", "ne", "net", "or", "org" };
111
112    static {
113        Arrays.sort(BAD_COUNTRY_2LDS);
114    }
115
116    /**
117     * Package level class to be accessed by cookie sync manager
118     */
119    static class Cookie {
120        static final byte MODE_NEW = 0;
121
122        static final byte MODE_NORMAL = 1;
123
124        static final byte MODE_DELETED = 2;
125
126        static final byte MODE_REPLACED = 3;
127
128        String domain;
129
130        String path;
131
132        String name;
133
134        String value;
135
136        long expires;
137
138        long lastAcessTime;
139
140        long lastUpdateTime;
141
142        boolean secure;
143
144        byte mode;
145
146        Cookie() {
147        }
148
149        Cookie(String defaultDomain, String defaultPath) {
150            domain = defaultDomain;
151            path = defaultPath;
152            expires = -1;
153        }
154
155        boolean exactMatch(Cookie in) {
156            // An exact match means that domain, path, and name are equal. If
157            // both values are null, the cookies match. If both values are
158            // non-null, the cookies match. If one value is null and the other
159            // is non-null, the cookies do not match (i.e. "foo=;" and "foo;")
160            boolean valuesMatch = !((value == null) ^ (in.value == null));
161            return domain.equals(in.domain) && path.equals(in.path) &&
162                    name.equals(in.name) && valuesMatch;
163        }
164
165        boolean domainMatch(String urlHost) {
166            if (domain.startsWith(".")) {
167                if (urlHost.endsWith(domain.substring(1))) {
168                    int len = domain.length();
169                    int urlLen = urlHost.length();
170                    if (urlLen > len - 1) {
171                        // make sure bar.com doesn't match .ar.com
172                        return urlHost.charAt(urlLen - len) == PERIOD;
173                    }
174                    return true;
175                }
176                return false;
177            } else {
178                // exact match if domain is not leading w/ dot
179                return urlHost.equals(domain);
180            }
181        }
182
183        boolean pathMatch(String urlPath) {
184            if (urlPath.startsWith(path)) {
185                int len = path.length();
186                if (len == 0) {
187                    Log.w(LOGTAG, "Empty cookie path");
188                    return false;
189                }
190                int urlLen = urlPath.length();
191                if (path.charAt(len-1) != PATH_DELIM && urlLen > len) {
192                    // make sure /wee doesn't match /we
193                    return urlPath.charAt(len) == PATH_DELIM;
194                }
195                return true;
196            }
197            return false;
198        }
199
200        public String toString() {
201            return "domain: " + domain + "; path: " + path + "; name: " + name
202                    + "; value: " + value;
203        }
204    }
205
206    private static final CookieComparator COMPARATOR = new CookieComparator();
207
208    private static final class CookieComparator implements Comparator<Cookie> {
209        public int compare(Cookie cookie1, Cookie cookie2) {
210            // According to RFC 2109, multiple cookies are ordered in a way such
211            // that those with more specific Path attributes precede those with
212            // less specific. Ordering with respect to other attributes (e.g.,
213            // Domain) is unspecified.
214            // As Set is not modified if the two objects are same, we do want to
215            // assign different value for each cookie.
216            int diff = cookie2.path.length() - cookie1.path.length();
217            if (diff != 0) return diff;
218
219            diff = cookie2.domain.length() - cookie1.domain.length();
220            if (diff != 0) return diff;
221
222            // If cookie2 has a null value, it should come later in
223            // the list.
224            if (cookie2.value == null) {
225                // If both cookies have null values, fall back to using the name
226                // difference.
227                if (cookie1.value != null) {
228                    return -1;
229                }
230            } else if (cookie1.value == null) {
231                // Now we know that cookie2 does not have a null value, if
232                // cookie1 has a null value, place it later in the list.
233                return 1;
234            }
235
236            // Fallback to comparing the name to ensure consistent order.
237            return cookie1.name.compareTo(cookie2.name);
238        }
239    }
240
241    private CookieManager() {
242    }
243
244    protected Object clone() throws CloneNotSupportedException {
245        throw new CloneNotSupportedException("doesn't implement Cloneable");
246    }
247
248    /**
249     * Get a singleton CookieManager. If this is called before any
250     * {@link WebView} is created or outside of {@link WebView} context, the
251     * caller needs to call {@link CookieSyncManager#createInstance(Context)}
252     * first.
253     *
254     * @return CookieManager
255     */
256    public static synchronized CookieManager getInstance() {
257        if (sRef == null) {
258            sRef = new CookieManager();
259        }
260        return sRef;
261    }
262
263    /**
264     * Control whether cookie is enabled or disabled
265     * @param accept TRUE if accept cookie
266     */
267    public synchronized void setAcceptCookie(boolean accept) {
268        if (JniUtil.useChromiumHttpStack()) {
269            nativeSetAcceptCookie(accept);
270            return;
271        }
272
273        mAcceptCookie = accept;
274    }
275
276    /**
277     * Return whether cookie is enabled
278     * @return TRUE if accept cookie
279     */
280    public synchronized boolean acceptCookie() {
281        if (JniUtil.useChromiumHttpStack()) {
282            return nativeAcceptCookie();
283        }
284
285        return mAcceptCookie;
286    }
287
288    /**
289     * Set cookie for a given url. The old cookie with same host/path/name will
290     * be removed. The new cookie will be added if it is not expired or it does
291     * not have expiration which implies it is session cookie.
292     * @param url The url which cookie is set for
293     * @param value The value for set-cookie: in http response header
294     */
295    public void setCookie(String url, String value) {
296        if (JniUtil.useChromiumHttpStack()) {
297            setCookie(url, value, false);
298            return;
299        }
300
301        WebAddress uri;
302        try {
303            uri = new WebAddress(url);
304        } catch (ParseException ex) {
305            Log.e(LOGTAG, "Bad address: " + url);
306            return;
307        }
308
309        setCookie(uri, value);
310    }
311
312    /**
313     * Set cookie for a given url. The old cookie with same host/path/name will
314     * be removed. The new cookie will be added if it is not expired or it does
315     * not have expiration which implies it is session cookie.
316     * @param url The url which cookie is set for
317     * @param value The value for set-cookie: in http response header
318     * @param privateBrowsing cookie jar to use
319     * @hide hiding private browsing
320     */
321    public void setCookie(String url, String value, boolean privateBrowsing) {
322        if (!JniUtil.useChromiumHttpStack()) {
323            setCookie(url, value);
324            return;
325        }
326
327        WebAddress uri;
328        try {
329            uri = new WebAddress(url);
330        } catch (ParseException ex) {
331            Log.e(LOGTAG, "Bad address: " + url);
332            return;
333        }
334
335        nativeSetCookie(uri.toString(), value, privateBrowsing);
336    }
337
338    /**
339     * Set cookie for a given uri. The old cookie with same host/path/name will
340     * be removed. The new cookie will be added if it is not expired or it does
341     * not have expiration which implies it is session cookie.
342     * @param uri The uri which cookie is set for
343     * @param value The value for set-cookie: in http response header
344     * @hide - hide this because it takes in a parameter of type WebAddress,
345     * a system private class.
346     */
347    public synchronized void setCookie(WebAddress uri, String value) {
348        if (JniUtil.useChromiumHttpStack()) {
349            nativeSetCookie(uri.toString(), value, false);
350            return;
351        }
352
353        if (value != null && value.length() > MAX_COOKIE_LENGTH) {
354            return;
355        }
356        if (!mAcceptCookie || uri == null) {
357            return;
358        }
359        if (DebugFlags.COOKIE_MANAGER) {
360            Log.v(LOGTAG, "setCookie: uri: " + uri + " value: " + value);
361        }
362
363        String[] hostAndPath = getHostAndPath(uri);
364        if (hostAndPath == null) {
365            return;
366        }
367
368        // For default path, when setting a cookie, the spec says:
369        //Path:   Defaults to the path of the request URL that generated the
370        // Set-Cookie response, up to, but not including, the
371        // right-most /.
372        if (hostAndPath[1].length() > 1) {
373            int index = hostAndPath[1].lastIndexOf(PATH_DELIM);
374            hostAndPath[1] = hostAndPath[1].substring(0,
375                    index > 0 ? index : index + 1);
376        }
377
378        ArrayList<Cookie> cookies = null;
379        try {
380            cookies = parseCookie(hostAndPath[0], hostAndPath[1], value);
381        } catch (RuntimeException ex) {
382            Log.e(LOGTAG, "parse cookie failed for: " + value);
383        }
384
385        if (cookies == null || cookies.size() == 0) {
386            return;
387        }
388
389        String baseDomain = getBaseDomain(hostAndPath[0]);
390        ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain);
391        if (cookieList == null) {
392            cookieList = CookieSyncManager.getInstance()
393                    .getCookiesForDomain(baseDomain);
394            mCookieMap.put(baseDomain, cookieList);
395        }
396
397        long now = System.currentTimeMillis();
398        int size = cookies.size();
399        for (int i = 0; i < size; i++) {
400            Cookie cookie = cookies.get(i);
401
402            boolean done = false;
403            Iterator<Cookie> iter = cookieList.iterator();
404            while (iter.hasNext()) {
405                Cookie cookieEntry = iter.next();
406                if (cookie.exactMatch(cookieEntry)) {
407                    // expires == -1 means no expires defined. Otherwise
408                    // negative means far future
409                    if (cookie.expires < 0 || cookie.expires > now) {
410                        // secure cookies can't be overwritten by non-HTTPS url
411                        if (!cookieEntry.secure || HTTPS.equals(uri.getScheme())) {
412                            cookieEntry.value = cookie.value;
413                            cookieEntry.expires = cookie.expires;
414                            cookieEntry.secure = cookie.secure;
415                            cookieEntry.lastAcessTime = now;
416                            cookieEntry.lastUpdateTime = now;
417                            cookieEntry.mode = Cookie.MODE_REPLACED;
418                        }
419                    } else {
420                        cookieEntry.lastUpdateTime = now;
421                        cookieEntry.mode = Cookie.MODE_DELETED;
422                    }
423                    done = true;
424                    break;
425                }
426            }
427
428            // expires == -1 means no expires defined. Otherwise negative means
429            // far future
430            if (!done && (cookie.expires < 0 || cookie.expires > now)) {
431                cookie.lastAcessTime = now;
432                cookie.lastUpdateTime = now;
433                cookie.mode = Cookie.MODE_NEW;
434                if (cookieList.size() > MAX_COOKIE_COUNT_PER_BASE_DOMAIN) {
435                    Cookie toDelete = new Cookie();
436                    toDelete.lastAcessTime = now;
437                    Iterator<Cookie> iter2 = cookieList.iterator();
438                    while (iter2.hasNext()) {
439                        Cookie cookieEntry2 = iter2.next();
440                        if ((cookieEntry2.lastAcessTime < toDelete.lastAcessTime)
441                                && cookieEntry2.mode != Cookie.MODE_DELETED) {
442                            toDelete = cookieEntry2;
443                        }
444                    }
445                    toDelete.mode = Cookie.MODE_DELETED;
446                }
447                cookieList.add(cookie);
448            }
449        }
450    }
451
452    /**
453     * Get cookie(s) for a given url so that it can be set to "cookie:" in http
454     * request header.
455     * @param url The url needs cookie
456     * @return The cookies in the format of NAME=VALUE [; NAME=VALUE]
457     */
458    public String getCookie(String url) {
459        if (JniUtil.useChromiumHttpStack()) {
460            return getCookie(url, false);
461        }
462
463        WebAddress uri;
464        try {
465            uri = new WebAddress(url);
466        } catch (ParseException ex) {
467            Log.e(LOGTAG, "Bad address: " + url);
468            return null;
469        }
470
471        return getCookie(uri);
472    }
473
474    /**
475     * Get cookie(s) for a given url so that it can be set to "cookie:" in http
476     * request header.
477     * @param url The url needs cookie
478     * @param privateBrowsing cookie jar to use
479     * @return The cookies in the format of NAME=VALUE [; NAME=VALUE]
480     * @hide Private mode is not very well exposed for now
481     */
482    public String getCookie(String url, boolean privateBrowsing) {
483        if (!JniUtil.useChromiumHttpStack()) {
484            // Just redirect to regular get cookie for android stack
485            return getCookie(url);
486        }
487
488        WebAddress uri;
489        try {
490            uri = new WebAddress(url);
491        } catch (ParseException ex) {
492            Log.e(LOGTAG, "Bad address: " + url);
493            return null;
494        }
495
496        return nativeGetCookie(uri.toString(), privateBrowsing);
497    }
498
499    /**
500     * Get cookie(s) for a given uri so that it can be set to "cookie:" in http
501     * request header.
502     * @param uri The uri needs cookie
503     * @return The cookies in the format of NAME=VALUE [; NAME=VALUE]
504     * @hide - hide this because it has a parameter of type WebAddress, which
505     * is a system private class.
506     */
507    public synchronized String getCookie(WebAddress uri) {
508        if (JniUtil.useChromiumHttpStack()) {
509            return nativeGetCookie(uri.toString(), false);
510        }
511
512        if (!mAcceptCookie || uri == null) {
513            return null;
514        }
515
516        String[] hostAndPath = getHostAndPath(uri);
517        if (hostAndPath == null) {
518            return null;
519        }
520
521        String baseDomain = getBaseDomain(hostAndPath[0]);
522        ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain);
523        if (cookieList == null) {
524            cookieList = CookieSyncManager.getInstance()
525                    .getCookiesForDomain(baseDomain);
526            mCookieMap.put(baseDomain, cookieList);
527        }
528
529        long now = System.currentTimeMillis();
530        boolean secure = HTTPS.equals(uri.getScheme());
531        Iterator<Cookie> iter = cookieList.iterator();
532
533        SortedSet<Cookie> cookieSet = new TreeSet<Cookie>(COMPARATOR);
534        while (iter.hasNext()) {
535            Cookie cookie = iter.next();
536            if (cookie.domainMatch(hostAndPath[0]) &&
537                    cookie.pathMatch(hostAndPath[1])
538                    // expires == -1 means no expires defined. Otherwise
539                    // negative means far future
540                    && (cookie.expires < 0 || cookie.expires > now)
541                    && (!cookie.secure || secure)
542                    && cookie.mode != Cookie.MODE_DELETED) {
543                cookie.lastAcessTime = now;
544                cookieSet.add(cookie);
545            }
546        }
547
548        StringBuilder ret = new StringBuilder(256);
549        Iterator<Cookie> setIter = cookieSet.iterator();
550        while (setIter.hasNext()) {
551            Cookie cookie = setIter.next();
552            if (ret.length() > 0) {
553                ret.append(SEMICOLON);
554                // according to RC2109, SEMICOLON is official separator,
555                // but when log in yahoo.com, it needs WHITE_SPACE too.
556                ret.append(WHITE_SPACE);
557            }
558
559            ret.append(cookie.name);
560            if (cookie.value != null) {
561                ret.append(EQUAL);
562                ret.append(cookie.value);
563            }
564        }
565
566        if (ret.length() > 0) {
567            if (DebugFlags.COOKIE_MANAGER) {
568                Log.v(LOGTAG, "getCookie: uri: " + uri + " value: " + ret);
569            }
570            return ret.toString();
571        } else {
572            if (DebugFlags.COOKIE_MANAGER) {
573                Log.v(LOGTAG, "getCookie: uri: " + uri
574                        + " But can't find cookie.");
575            }
576            return null;
577        }
578    }
579
580    /**
581     * Waits for pending operations to completed.
582     * {@hide}  Too late to release publically.
583     */
584    public void waitForCookieOperationsToComplete() {
585        // Note that this function is applicable for both the java
586        // and native http stacks, and works correctly with either.
587        synchronized (this) {
588            while (pendingCookieOperations > 0) {
589                try {
590                    wait();
591                } catch (InterruptedException e) { }
592            }
593        }
594    }
595
596    private synchronized void signalCookieOperationsComplete() {
597        pendingCookieOperations--;
598        assert pendingCookieOperations > -1;
599        notify();
600    }
601
602    private synchronized void signalCookieOperationsStart() {
603        pendingCookieOperations++;
604    }
605
606    /**
607     * Remove all session cookies, which are cookies without expiration date
608     */
609    public void removeSessionCookie() {
610        signalCookieOperationsStart();
611        if (JniUtil.useChromiumHttpStack()) {
612            new AsyncTask<Void, Void, Void>() {
613                protected Void doInBackground(Void... none) {
614                    nativeRemoveSessionCookie();
615                    signalCookieOperationsComplete();
616                    return null;
617                }
618            }.execute();
619            return;
620        }
621
622        final Runnable clearCache = new Runnable() {
623            public void run() {
624                synchronized(CookieManager.this) {
625                    Collection<ArrayList<Cookie>> cookieList = mCookieMap.values();
626                    Iterator<ArrayList<Cookie>> listIter = cookieList.iterator();
627                    while (listIter.hasNext()) {
628                        ArrayList<Cookie> list = listIter.next();
629                        Iterator<Cookie> iter = list.iterator();
630                        while (iter.hasNext()) {
631                            Cookie cookie = iter.next();
632                            if (cookie.expires == -1) {
633                                iter.remove();
634                            }
635                        }
636                    }
637                    CookieSyncManager.getInstance().clearSessionCookies();
638                    signalCookieOperationsComplete();
639                }
640            }
641        };
642        new Thread(clearCache).start();
643    }
644
645    /**
646     * Remove all cookies
647     */
648    public void removeAllCookie() {
649        if (JniUtil.useChromiumHttpStack()) {
650            nativeRemoveAllCookie();
651            return;
652        }
653
654        final Runnable clearCache = new Runnable() {
655            public void run() {
656                synchronized(CookieManager.this) {
657                    mCookieMap = new LinkedHashMap<String, ArrayList<Cookie>>(
658                            MAX_DOMAIN_COUNT, 0.75f, true);
659                    CookieSyncManager.getInstance().clearAllCookies();
660                }
661            }
662        };
663        new Thread(clearCache).start();
664    }
665
666    /**
667     *  Return true if there are stored cookies.
668     */
669    public synchronized boolean hasCookies() {
670        if (JniUtil.useChromiumHttpStack()) {
671            return hasCookies(false);
672        }
673
674        return CookieSyncManager.getInstance().hasCookies();
675    }
676
677    /**
678     *  Return true if there are stored cookies.
679     *  @param privateBrowsing cookie jar to use
680     *  @hide Hiding private mode
681     */
682    public synchronized boolean hasCookies(boolean privateBrowsing) {
683        if (!JniUtil.useChromiumHttpStack()) {
684            return hasCookies();
685        }
686
687        return nativeHasCookies(privateBrowsing);
688    }
689
690    /**
691     * Remove all expired cookies
692     */
693    public void removeExpiredCookie() {
694        if (JniUtil.useChromiumHttpStack()) {
695            nativeRemoveExpiredCookie();
696            return;
697        }
698
699        final Runnable clearCache = new Runnable() {
700            public void run() {
701                synchronized(CookieManager.this) {
702                    long now = System.currentTimeMillis();
703                    Collection<ArrayList<Cookie>> cookieList = mCookieMap.values();
704                    Iterator<ArrayList<Cookie>> listIter = cookieList.iterator();
705                    while (listIter.hasNext()) {
706                        ArrayList<Cookie> list = listIter.next();
707                        Iterator<Cookie> iter = list.iterator();
708                        while (iter.hasNext()) {
709                            Cookie cookie = iter.next();
710                            // expires == -1 means no expires defined. Otherwise
711                            // negative means far future
712                            if (cookie.expires > 0 && cookie.expires < now) {
713                                iter.remove();
714                            }
715                        }
716                    }
717                    CookieSyncManager.getInstance().clearExpiredCookies(now);
718                }
719            }
720        };
721        new Thread(clearCache).start();
722    }
723
724    /**
725     * Package level api, called from CookieSyncManager
726     *
727     * Flush all cookies managed by the Chrome HTTP stack to flash.
728     */
729    void flushCookieStore() {
730        if (JniUtil.useChromiumHttpStack()) {
731            nativeFlushCookieStore();
732        }
733    }
734
735    /**
736     * Whether cookies are accepted for file scheme URLs.
737     */
738    public static boolean allowFileSchemeCookies() {
739        if (JniUtil.useChromiumHttpStack()) {
740            return nativeAcceptFileSchemeCookies();
741        } else {
742            return true;
743        }
744    }
745
746    /**
747     * Sets whether cookies are accepted for file scheme URLs.
748     *
749     * Use of cookies with file scheme URLs is potentially insecure. Do not use this feature unless
750     * you can be sure that no unintentional sharing of cookie data can take place.
751     * <p>
752     * Note that calls to this method will have no effect if made after a WebView or CookieManager
753     * instance has been created.
754     */
755    public static void setAcceptFileSchemeCookies(boolean accept) {
756        if (JniUtil.useChromiumHttpStack()) {
757            nativeSetAcceptFileSchemeCookies(accept);
758        }
759    }
760
761    /**
762     * Package level api, called from CookieSyncManager
763     *
764     * Get a list of cookies which are updated since a given time.
765     * @param last The given time in millisec
766     * @return A list of cookies
767     */
768    synchronized ArrayList<Cookie> getUpdatedCookiesSince(long last) {
769        ArrayList<Cookie> cookies = new ArrayList<Cookie>();
770        Collection<ArrayList<Cookie>> cookieList = mCookieMap.values();
771        Iterator<ArrayList<Cookie>> listIter = cookieList.iterator();
772        while (listIter.hasNext()) {
773            ArrayList<Cookie> list = listIter.next();
774            Iterator<Cookie> iter = list.iterator();
775            while (iter.hasNext()) {
776                Cookie cookie = iter.next();
777                if (cookie.lastUpdateTime > last) {
778                    cookies.add(cookie);
779                }
780            }
781        }
782        return cookies;
783    }
784
785    /**
786     * Package level api, called from CookieSyncManager
787     *
788     * Delete a Cookie in the RAM
789     * @param cookie Cookie to be deleted
790     */
791    synchronized void deleteACookie(Cookie cookie) {
792        if (cookie.mode == Cookie.MODE_DELETED) {
793            String baseDomain = getBaseDomain(cookie.domain);
794            ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain);
795            if (cookieList != null) {
796                cookieList.remove(cookie);
797                if (cookieList.isEmpty()) {
798                    mCookieMap.remove(baseDomain);
799                }
800            }
801        }
802    }
803
804    /**
805     * Package level api, called from CookieSyncManager
806     *
807     * Called after a cookie is synced to FLASH
808     * @param cookie Cookie to be synced
809     */
810    synchronized void syncedACookie(Cookie cookie) {
811        cookie.mode = Cookie.MODE_NORMAL;
812    }
813
814    /**
815     * Package level api, called from CookieSyncManager
816     *
817     * Delete the least recent used domains if the total cookie count in RAM
818     * exceeds the limit
819     * @return A list of cookies which are removed from RAM
820     */
821    synchronized ArrayList<Cookie> deleteLRUDomain() {
822        int count = 0;
823        int byteCount = 0;
824        int mapSize = mCookieMap.size();
825
826        if (mapSize < MAX_RAM_DOMAIN_COUNT) {
827            Collection<ArrayList<Cookie>> cookieLists = mCookieMap.values();
828            Iterator<ArrayList<Cookie>> listIter = cookieLists.iterator();
829            while (listIter.hasNext() && count < MAX_RAM_COOKIES_COUNT) {
830                ArrayList<Cookie> list = listIter.next();
831                if (DebugFlags.COOKIE_MANAGER) {
832                    Iterator<Cookie> iter = list.iterator();
833                    while (iter.hasNext() && count < MAX_RAM_COOKIES_COUNT) {
834                        Cookie cookie = iter.next();
835                        // 14 is 3 * sizeof(long) + sizeof(boolean)
836                        // + sizeof(byte)
837                        byteCount += cookie.domain.length()
838                                + cookie.path.length()
839                                + cookie.name.length()
840                                + (cookie.value != null
841                                        ? cookie.value.length()
842                                        : 0)
843                                + 14;
844                        count++;
845                    }
846                } else {
847                    count += list.size();
848                }
849            }
850        }
851
852        ArrayList<Cookie> retlist = new ArrayList<Cookie>();
853        if (mapSize >= MAX_RAM_DOMAIN_COUNT || count >= MAX_RAM_COOKIES_COUNT) {
854            if (DebugFlags.COOKIE_MANAGER) {
855                Log.v(LOGTAG, count + " cookies used " + byteCount
856                        + " bytes with " + mapSize + " domains");
857            }
858            Object[] domains = mCookieMap.keySet().toArray();
859            int toGo = mapSize / 10 + 1;
860            while (toGo-- > 0){
861                String domain = domains[toGo].toString();
862                if (DebugFlags.COOKIE_MANAGER) {
863                    Log.v(LOGTAG, "delete domain: " + domain
864                            + " from RAM cache");
865                }
866                retlist.addAll(mCookieMap.get(domain));
867                mCookieMap.remove(domain);
868            }
869        }
870        return retlist;
871    }
872
873    /**
874     * Extract the host and path out of a uri
875     * @param uri The given WebAddress
876     * @return The host and path in the format of String[], String[0] is host
877     *          which has at least two periods, String[1] is path which always
878     *          ended with "/"
879     */
880    private String[] getHostAndPath(WebAddress uri) {
881        if (uri.getHost() != null && uri.getPath() != null) {
882
883            /*
884             * The domain (i.e. host) portion of the cookie is supposed to be
885             * case-insensitive. We will consistently return the domain in lower
886             * case, which allows us to do the more efficient equals comparison
887             * instead of equalIgnoreCase.
888             *
889             * See: http://www.ieft.org/rfc/rfc2965.txt (Section 3.3.3)
890             */
891            String[] ret = new String[2];
892            ret[0] = uri.getHost().toLowerCase();
893            ret[1] = uri.getPath();
894
895            int index = ret[0].indexOf(PERIOD);
896            if (index == -1) {
897                if (uri.getScheme().equalsIgnoreCase("file")) {
898                    // There is a potential bug where a local file path matches
899                    // another file in the local web server directory. Still
900                    // "localhost" is the best pseudo domain name.
901                    ret[0] = "localhost";
902                }
903            } else if (index == ret[0].lastIndexOf(PERIOD)) {
904                // cookie host must have at least two periods
905                ret[0] = PERIOD + ret[0];
906            }
907
908            if (ret[1].charAt(0) != PATH_DELIM) {
909                return null;
910            }
911
912            /*
913             * find cookie path, e.g. for http://www.google.com, the path is "/"
914             * for http://www.google.com/lab/, the path is "/lab"
915             * for http://www.google.com/lab/foo, the path is "/lab/foo"
916             * for http://www.google.com/lab?hl=en, the path is "/lab"
917             * for http://www.google.com/lab.asp?hl=en, the path is "/lab.asp"
918             * Note: the path from URI has at least one "/"
919             * See:
920             * http://www.unix.com.ua/rfc/rfc2109.html
921             */
922            index = ret[1].indexOf(QUESTION_MARK);
923            if (index != -1) {
924                ret[1] = ret[1].substring(0, index);
925            }
926
927            return ret;
928        } else
929            return null;
930    }
931
932    /**
933     * Get the base domain for a give host. E.g. mail.google.com will return
934     * google.com
935     * @param host The give host
936     * @return the base domain
937     */
938    private String getBaseDomain(String host) {
939        int startIndex = 0;
940        int nextIndex = host.indexOf(PERIOD);
941        int lastIndex = host.lastIndexOf(PERIOD);
942        while (nextIndex < lastIndex) {
943            startIndex = nextIndex + 1;
944            nextIndex = host.indexOf(PERIOD, startIndex);
945        }
946        if (startIndex > 0) {
947            return host.substring(startIndex);
948        } else {
949            return host;
950        }
951    }
952
953    /**
954     * parseCookie() parses the cookieString which is a comma-separated list of
955     * one or more cookies in the format of "NAME=VALUE; expires=DATE;
956     * path=PATH; domain=DOMAIN_NAME; secure httponly" to a list of Cookies.
957     * Here is a sample: IGDND=1, IGPC=ET=UB8TSNwtDmQ:AF=0; expires=Sun,
958     * 17-Jan-2038 19:14:07 GMT; path=/ig; domain=.google.com, =,
959     * PREF=ID=408909b1b304593d:TM=1156459854:LM=1156459854:S=V-vCAU6Sh-gobCfO;
960     * expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com which
961     * contains 3 cookies IGDND, IGPC, PREF and an empty cookie
962     * @param host The default host
963     * @param path The default path
964     * @param cookieString The string coming from "Set-Cookie:"
965     * @return A list of Cookies
966     */
967    private ArrayList<Cookie> parseCookie(String host, String path,
968            String cookieString) {
969        ArrayList<Cookie> ret = new ArrayList<Cookie>();
970
971        int index = 0;
972        int length = cookieString.length();
973        while (true) {
974            Cookie cookie = null;
975
976            // done
977            if (index < 0 || index >= length) {
978                break;
979            }
980
981            // skip white space
982            if (cookieString.charAt(index) == WHITE_SPACE) {
983                index++;
984                continue;
985            }
986
987            /*
988             * get NAME=VALUE; pair. detecting the end of a pair is tricky, it
989             * can be the end of a string, like "foo=bluh", it can be semicolon
990             * like "foo=bluh;path=/"; or it can be enclosed by \", like
991             * "foo=\"bluh bluh\";path=/"
992             *
993             * Note: in the case of "foo=bluh, bar=bluh;path=/", we interpret
994             * it as one cookie instead of two cookies.
995             */
996            int semicolonIndex = cookieString.indexOf(SEMICOLON, index);
997            int equalIndex = cookieString.indexOf(EQUAL, index);
998            cookie = new Cookie(host, path);
999
1000            // Cookies like "testcookie; path=/;" are valid and used
1001            // (lovefilm.se).
1002            // Look for 2 cases:
1003            // 1. "foo" or "foo;" where equalIndex is -1
1004            // 2. "foo; path=..." where the first semicolon is before an equal
1005            //    and a semicolon exists.
1006            if ((semicolonIndex != -1 && (semicolonIndex < equalIndex)) ||
1007                    equalIndex == -1) {
1008                // Fix up the index in case we have a string like "testcookie"
1009                if (semicolonIndex == -1) {
1010                    semicolonIndex = length;
1011                }
1012                cookie.name = cookieString.substring(index, semicolonIndex);
1013                cookie.value = null;
1014            } else {
1015                cookie.name = cookieString.substring(index, equalIndex);
1016                // Make sure we do not throw an exception if the cookie is like
1017                // "foo="
1018                if ((equalIndex < length - 1) &&
1019                        (cookieString.charAt(equalIndex + 1) == QUOTATION)) {
1020                    index = cookieString.indexOf(QUOTATION, equalIndex + 2);
1021                    if (index == -1) {
1022                        // bad format, force return
1023                        break;
1024                    }
1025                }
1026                // Get the semicolon index again in case it was contained within
1027                // the quotations.
1028                semicolonIndex = cookieString.indexOf(SEMICOLON, index);
1029                if (semicolonIndex == -1) {
1030                    semicolonIndex = length;
1031                }
1032                if (semicolonIndex - equalIndex > MAX_COOKIE_LENGTH) {
1033                    // cookie is too big, trim it
1034                    cookie.value = cookieString.substring(equalIndex + 1,
1035                            equalIndex + 1 + MAX_COOKIE_LENGTH);
1036                } else if (equalIndex + 1 == semicolonIndex
1037                        || semicolonIndex < equalIndex) {
1038                    // this is an unusual case like "foo=;" or "foo="
1039                    cookie.value = "";
1040                } else {
1041                    cookie.value = cookieString.substring(equalIndex + 1,
1042                            semicolonIndex);
1043                }
1044            }
1045            // get attributes
1046            index = semicolonIndex;
1047            while (true) {
1048                // done
1049                if (index < 0 || index >= length) {
1050                    break;
1051                }
1052
1053                // skip white space and semicolon
1054                if (cookieString.charAt(index) == WHITE_SPACE
1055                        || cookieString.charAt(index) == SEMICOLON) {
1056                    index++;
1057                    continue;
1058                }
1059
1060                // comma means next cookie
1061                if (cookieString.charAt(index) == COMMA) {
1062                    index++;
1063                    break;
1064                }
1065
1066                // "secure" is a known attribute doesn't use "=";
1067                // while sites like live.com uses "secure="
1068                if (length - index >= SECURE_LENGTH
1069                        && cookieString.substring(index, index + SECURE_LENGTH).
1070                        equalsIgnoreCase(SECURE)) {
1071                    index += SECURE_LENGTH;
1072                    cookie.secure = true;
1073                    if (index == length) break;
1074                    if (cookieString.charAt(index) == EQUAL) index++;
1075                    continue;
1076                }
1077
1078                // "httponly" is a known attribute doesn't use "=";
1079                // while sites like live.com uses "httponly="
1080                if (length - index >= HTTP_ONLY_LENGTH
1081                        && cookieString.substring(index,
1082                            index + HTTP_ONLY_LENGTH).
1083                        equalsIgnoreCase(HTTP_ONLY)) {
1084                    index += HTTP_ONLY_LENGTH;
1085                    if (index == length) break;
1086                    if (cookieString.charAt(index) == EQUAL) index++;
1087                    // FIXME: currently only parse the attribute
1088                    continue;
1089                }
1090                equalIndex = cookieString.indexOf(EQUAL, index);
1091                if (equalIndex > 0) {
1092                    String name = cookieString.substring(index, equalIndex).toLowerCase();
1093                    int valueIndex = equalIndex + 1;
1094                    while (valueIndex < length && cookieString.charAt(valueIndex) == WHITE_SPACE) {
1095                        valueIndex++;
1096                    }
1097
1098                    if (name.equals(EXPIRES)) {
1099                        int comaIndex = cookieString.indexOf(COMMA, equalIndex);
1100
1101                        // skip ',' in (Wdy, DD-Mon-YYYY HH:MM:SS GMT) or
1102                        // (Weekday, DD-Mon-YY HH:MM:SS GMT) if it applies.
1103                        // "Wednesday" is the longest Weekday which has length 9
1104                        if ((comaIndex != -1) &&
1105                                (comaIndex - valueIndex <= 10)) {
1106                            index = comaIndex + 1;
1107                        }
1108                    }
1109                    semicolonIndex = cookieString.indexOf(SEMICOLON, index);
1110                    int commaIndex = cookieString.indexOf(COMMA, index);
1111                    if (semicolonIndex == -1 && commaIndex == -1) {
1112                        index = length;
1113                    } else if (semicolonIndex == -1) {
1114                        index = commaIndex;
1115                    } else if (commaIndex == -1) {
1116                        index = semicolonIndex;
1117                    } else {
1118                        index = Math.min(semicolonIndex, commaIndex);
1119                    }
1120                    String value = cookieString.substring(valueIndex, index);
1121
1122                    // Strip quotes if they exist
1123                    if (value.length() > 2 && value.charAt(0) == QUOTATION) {
1124                        int endQuote = value.indexOf(QUOTATION, 1);
1125                        if (endQuote > 0) {
1126                            value = value.substring(1, endQuote);
1127                        }
1128                    }
1129                    if (name.equals(EXPIRES)) {
1130                        try {
1131                            cookie.expires = AndroidHttpClient.parseDate(value);
1132                        } catch (IllegalArgumentException ex) {
1133                            Log.e(LOGTAG,
1134                                    "illegal format for expires: " + value);
1135                        }
1136                    } else if (name.equals(MAX_AGE)) {
1137                        try {
1138                            cookie.expires = System.currentTimeMillis() + 1000
1139                                    * Long.parseLong(value);
1140                        } catch (NumberFormatException ex) {
1141                            Log.e(LOGTAG,
1142                                    "illegal format for max-age: " + value);
1143                        }
1144                    } else if (name.equals(PATH)) {
1145                        // only allow non-empty path value
1146                        if (value.length() > 0) {
1147                            cookie.path = value;
1148                        }
1149                    } else if (name.equals(DOMAIN)) {
1150                        int lastPeriod = value.lastIndexOf(PERIOD);
1151                        if (lastPeriod == 0) {
1152                            // disallow cookies set for TLDs like [.com]
1153                            cookie.domain = null;
1154                            continue;
1155                        }
1156                        try {
1157                            Integer.parseInt(value.substring(lastPeriod + 1));
1158                            // no wildcard for ip address match
1159                            if (!value.equals(host)) {
1160                                // no cross-site cookie
1161                                cookie.domain = null;
1162                            }
1163                            continue;
1164                        } catch (NumberFormatException ex) {
1165                            // ignore the exception, value is a host name
1166                        }
1167                        value = value.toLowerCase();
1168                        if (value.charAt(0) != PERIOD) {
1169                            // pre-pended dot to make it as a domain cookie
1170                            value = PERIOD + value;
1171                            lastPeriod++;
1172                        }
1173                        if (host.endsWith(value.substring(1))) {
1174                            int len = value.length();
1175                            int hostLen = host.length();
1176                            if (hostLen > (len - 1)
1177                                    && host.charAt(hostLen - len) != PERIOD) {
1178                                // make sure the bar.com doesn't match .ar.com
1179                                cookie.domain = null;
1180                                continue;
1181                            }
1182                            // disallow cookies set on ccTLDs like [.co.uk]
1183                            if ((len == lastPeriod + 3)
1184                                    && (len >= 6 && len <= 8)) {
1185                                String s = value.substring(1, lastPeriod);
1186                                if (Arrays.binarySearch(BAD_COUNTRY_2LDS, s) >= 0) {
1187                                    cookie.domain = null;
1188                                    continue;
1189                                }
1190                            }
1191                            cookie.domain = value;
1192                        } else {
1193                            // no cross-site or more specific sub-domain cookie
1194                            cookie.domain = null;
1195                        }
1196                    }
1197                } else {
1198                    // bad format, force return
1199                    index = length;
1200                }
1201            }
1202            if (cookie != null && cookie.domain != null) {
1203                ret.add(cookie);
1204            }
1205        }
1206        return ret;
1207    }
1208
1209    // Native functions
1210    private static native boolean nativeAcceptCookie();
1211    private static native String nativeGetCookie(String url, boolean privateBrowsing);
1212    private static native boolean nativeHasCookies(boolean privateBrowsing);
1213    private static native void nativeRemoveAllCookie();
1214    private static native void nativeRemoveExpiredCookie();
1215    private static native void nativeRemoveSessionCookie();
1216    private static native void nativeSetAcceptCookie(boolean accept);
1217    private static native void nativeSetCookie(String url, String value, boolean privateBrowsing);
1218    private static native void nativeFlushCookieStore();
1219    private static native boolean nativeAcceptFileSchemeCookies();
1220    private static native void nativeSetAcceptFileSchemeCookies(boolean accept);
1221}
1222