package org.wikipedia.dataclient; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.util.ArraySet; import android.text.TextUtils; import org.wikipedia.login.User; import org.wikipedia.settings.Prefs; import org.wikipedia.util.StringUtil; import java.io.IOException; import java.net.CookieManager; import java.net.CookieStore; import java.net.HttpCookie; import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; public final class SharedPreferenceCookieManager extends CookieManager { private static final String DELIMITER = ";"; private static final String CENTRALAUTH_PREFIX = "centralauth_"; private final Map<String, Map<String, String>> cookieJar = new HashMap<>(); private static SharedPreferenceCookieManager INSTANCE; @NonNull public static SharedPreferenceCookieManager getInstance() { if (INSTANCE == null) { INSTANCE = new SharedPreferenceCookieManager(); } return INSTANCE; } private SharedPreferenceCookieManager() { List<String> domains = Prefs.getCookieDomainsAsList(); for (String domain: domains) { String cookies = Prefs.getCookiesForDomain(domain); cookieJar.put(domain, makeCookieMap(makeList(cookies))); } } @Override public synchronized Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders) throws IOException { if (uri == null || requestHeaders == null) { throw new IllegalArgumentException("Argument is null"); } Map<String, List<String>> cookieMap = new HashMap<>(); List<String> cookiesList = new ArrayList<>(); String domain = uri.getAuthority(); for (String domainSpec: cookieJar.keySet()) { // For sites outside the wikipedia.org domain, like wikidata.org, // transfer the centralauth cookies from wikipedia.org, too, if the user is logged in if (User.isLoggedIn() && domain.equals("www.wikidata.org") && domainSpec.endsWith("wikipedia.org")) { cookiesList.addAll(makeCookieList(cookieJar.get(domainSpec), CENTRALAUTH_PREFIX)); } // Very weak domain matching. // Primarily to make sure that cookies set for .wikipedia.org are sent for // en.wikipedia.org and *.wikimedia.org // FIXME: Whitelist the domains we accept cookies from/send cookies to. SECURITY!!!1 if (domain.endsWith(domainSpec) || (domain.endsWith(".wikimedia.org") && domainSpec.endsWith("wikipedia.org"))) { cookiesList.addAll(makeCookieList(cookieJar.get(domainSpec))); } } cookieMap.put("Cookie", cookiesList); return Collections.unmodifiableMap(cookieMap); } @Override public synchronized void put(URI uri, Map<String, List<String>> responseHeaders) throws IOException { // pre-condition check if (uri == null || responseHeaders == null) { throw new IllegalArgumentException("Argument is null"); } ArraySet<String> domainsModified = new ArraySet<>(); for (String headerKey : responseHeaders.keySet()) { if (headerKey == null || !headerKey.equalsIgnoreCase("Set-Cookie")) { continue; } for (String headerValue : responseHeaders.get(headerKey)) { try { List<HttpCookie> cookies = HttpCookie.parse(headerValue); for (HttpCookie cookie : cookies) { // Default to the URI's domain if domain is not explicitly set String domainSpec = cookie.getDomain() == null ? uri.getAuthority() : cookie.getDomain(); if (!cookieJar.containsKey(domainSpec)) { cookieJar.put(domainSpec, new HashMap<String, String>()); } if (cookie.hasExpired() || "deleted".equals(cookie.getValue())) { cookieJar.get(domainSpec).remove(cookie.getName()); } else { cookieJar.get(domainSpec).put(cookie.getName(), cookie.getValue()); } domainsModified.add(domainSpec); } } catch (IllegalArgumentException e) { // invalid set-cookie header string // no-op } } } Prefs.setCookieDomains(makeString(cookieJar.keySet())); for (String domain : domainsModified) { Prefs.setCookiesForDomain(domain, makeString(makeCookieList(cookieJar.get(domain)))); } } @Override public CookieStore getCookieStore() { // We don't actually have one. hehe throw new UnsupportedOperationException("We poor. We no have CookieStore"); } public synchronized void clearAllCookies() { for (String domain: cookieJar.keySet()) { Prefs.removeCookiesForDomain(domain); } Prefs.setCookieDomains(null); cookieJar.clear(); } public static List<String> makeList(String str) { return StringUtil.delimiterStringToList(str, DELIMITER); } @Nullable public synchronized String getCookieByName(@NonNull String name) { for (String domainSpec: cookieJar.keySet()) { for (String cookie : cookieJar.get(domainSpec).keySet()) { if (cookie.equals(name)) { return cookieJar.get(domainSpec).get(cookie); } } } return null; } private Map<String, String> makeCookieMap(@NonNull List<String> cookies) { Map<String, String> cookiesMap = new HashMap<>(); for (String cookie : cookies) { if (!cookie.contains("=")) { throw new RuntimeException("Cookie " + cookie + " is invalid!"); } String[] parts = cookie.split("="); cookiesMap.put(parts[0], parts[1]); } return cookiesMap; } private List<String> makeCookieList(@NonNull Map<String, String> cookies) { return makeCookieList(cookies, null); } private List<String> makeCookieList(@NonNull Map<String, String> cookies, @Nullable String prefixFilter) { List<String> cookiesList = new ArrayList<>(); for (Map.Entry<String, String> entry: cookies.entrySet()) { if (prefixFilter == null || entry.getKey().startsWith(prefixFilter)) { cookiesList.add(entry.getKey() + "=" + entry.getValue()); } } return cookiesList; } private String makeString(@NonNull Iterable<String> list) { return TextUtils.join(DELIMITER, list); } }