// vim:ts=2:sw=2:et:
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is cookie manager code.
 *
 * The Initial Developer of the Original Code is
 * Michiel van Leeuwen (mvl@exedo.nl).
 * Portions created by the Initial Developer are Copyright (C) 2003
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Darin Fisher <darin@meer.net>
 *   Daniel Witte <dwitte@stanford.edu>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


#include "nsCookiePermission.h"
#include "nsICookie2.h"
#include "nsIServiceManager.h"
#include "nsICookiePromptService.h"
#include "nsICookieManager2.h"
#include "nsNetUtil.h"
#include "nsIURI.h"
#include "nsIPrefService.h"
#include "nsIPrefBranch.h"
#include "nsIPrefBranch2.h"
#include "nsIDocShell.h"
#include "nsIDocShellTreeItem.h"
#include "nsIWebNavigation.h"
#include "nsINode.h"
#include "nsIChannel.h"
#include "nsIDOMWindow.h"
#include "nsIDOMDocument.h"
#include "nsIPrincipal.h"
#include "nsString.h"
#include "nsCRT.h"

#include "flockIChannel.h"

/****************************************************************
 ************************ nsCookiePermission ********************
 ****************************************************************/

// values for mCookiesLifetimePolicy
// 0 == accept normally
// 1 == ask before accepting
// 2 == downgrade to session
// 3 == limit lifetime to N days
static const PRUint32 ACCEPT_NORMALLY = 0;
static const PRUint32 ASK_BEFORE_ACCEPT = 1;
static const PRUint32 ACCEPT_SESSION = 2;
static const PRUint32 ACCEPT_FOR_N_DAYS = 3;

static const PRBool kDefaultPolicy = PR_TRUE;
static const char kCookiesLifetimePolicy[] = "network.cookie.lifetimePolicy";
static const char kCookiesLifetimeDays[] = "network.cookie.lifetime.days";
static const char kCookiesAlwaysAcceptSession[] = "network.cookie.alwaysAcceptSessionCookies";
#ifdef MOZ_MAIL_NEWS
static const char kCookiesDisabledForMailNews[] = "network.cookie.disableCookieForMailNews";
#endif

static const char kCookiesPrefsMigrated[] = "network.cookie.prefsMigrated";
// obsolete pref names for migration
static const char kCookiesLifetimeEnabled[] = "network.cookie.lifetime.enabled";
static const char kCookiesLifetimeBehavior[] = "network.cookie.lifetime.behavior";
static const char kCookiesAskPermission[] = "network.cookie.warnAboutCookies";

static const char kPermissionType[] = "cookie";

#ifdef MOZ_MAIL_NEWS
// returns PR_TRUE if URI appears to be the URI of a mailnews protocol
static PRBool
IsFromMailNews(nsIURI *aURI)
{
  static const char *kMailNewsProtocols[] =
      { "imap", "news", "snews", "mailbox", nsnull };
  PRBool result;
  for (const char **p = kMailNewsProtocols; *p; ++p) {
    if (NS_SUCCEEDED(aURI->SchemeIs(*p, &result)) && result)
      return PR_TRUE;
  }
  return PR_FALSE;
}
#endif

NS_IMPL_ISUPPORTS2(nsCookiePermission,
                   nsICookiePermission,
                   nsIObserver)

nsresult
nsCookiePermission::Init()
{
  nsresult rv;
  mPermMgr = do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
  if (NS_FAILED(rv)) return rv;

  // failure to access the pref service is non-fatal...
  nsCOMPtr<nsIPrefBranch2> prefBranch =
      do_GetService(NS_PREFSERVICE_CONTRACTID);
  if (prefBranch) {
    prefBranch->AddObserver(kCookiesLifetimePolicy, this, PR_FALSE);
    prefBranch->AddObserver(kCookiesLifetimeDays, this, PR_FALSE);
    prefBranch->AddObserver(kCookiesAlwaysAcceptSession, this, PR_FALSE);
#ifdef MOZ_MAIL_NEWS
    prefBranch->AddObserver(kCookiesDisabledForMailNews, this, PR_FALSE);
#endif
    PrefChanged(prefBranch, nsnull);

    // migration code for original cookie prefs
    PRBool migrated;
    rv = prefBranch->GetBoolPref(kCookiesPrefsMigrated, &migrated);
    if (NS_FAILED(rv) || !migrated) {
      PRBool warnAboutCookies = PR_FALSE;
      prefBranch->GetBoolPref(kCookiesAskPermission, &warnAboutCookies);

      // if the user is using ask before accepting, we'll use that
      if (warnAboutCookies)
        prefBranch->SetIntPref(kCookiesLifetimePolicy, ASK_BEFORE_ACCEPT);
        
      PRBool lifetimeEnabled = PR_FALSE;
      prefBranch->GetBoolPref(kCookiesLifetimeEnabled, &lifetimeEnabled);
      
      // if they're limiting lifetime and not using the prompts, use the 
      // appropriate limited lifetime pref
      if (lifetimeEnabled && !warnAboutCookies) {
        PRInt32 lifetimeBehavior;
        prefBranch->GetIntPref(kCookiesLifetimeBehavior, &lifetimeBehavior);
        if (lifetimeBehavior)
          prefBranch->SetIntPref(kCookiesLifetimePolicy, ACCEPT_FOR_N_DAYS);
        else
          prefBranch->SetIntPref(kCookiesLifetimePolicy, ACCEPT_SESSION);
      }
      prefBranch->SetBoolPref(kCookiesPrefsMigrated, PR_TRUE);
    }
  }

  return NS_OK;
}

void
nsCookiePermission::PrefChanged(nsIPrefBranch *aPrefBranch,
                                const char    *aPref)
{
  PRInt32 val;

#define PREF_CHANGED(_P) (!aPref || !strcmp(aPref, _P))

  if (PREF_CHANGED(kCookiesLifetimePolicy) &&
      NS_SUCCEEDED(aPrefBranch->GetIntPref(kCookiesLifetimePolicy, &val)))
    mCookiesLifetimePolicy = val;

  if (PREF_CHANGED(kCookiesLifetimeDays) &&
      NS_SUCCEEDED(aPrefBranch->GetIntPref(kCookiesLifetimeDays, &val)))
    // save cookie lifetime in seconds instead of days
    mCookiesLifetimeSec = val * 24 * 60 * 60;

  if (PREF_CHANGED(kCookiesAlwaysAcceptSession) &&
      NS_SUCCEEDED(aPrefBranch->GetBoolPref(kCookiesAlwaysAcceptSession, &val)))
    mCookiesAlwaysAcceptSession = val;

#ifdef MOZ_MAIL_NEWS
  if (PREF_CHANGED(kCookiesDisabledForMailNews) &&
      NS_SUCCEEDED(aPrefBranch->GetBoolPref(kCookiesDisabledForMailNews, &val)))
    mCookiesDisabledForMailNews = val;
#endif
}

NS_IMETHODIMP
nsCookiePermission::SetAccess(nsIURI         *aURI,
                              nsCookieAccess  aAccess)
{
  //
  // NOTE: nsCookieAccess values conveniently match up with
  //       the permission codes used by nsIPermissionManager.
  //       this is nice because it avoids conversion code.
  //
  return mPermMgr->Add(aURI, kPermissionType, aAccess);
}

NS_IMETHODIMP
nsCookiePermission::CanAccess(nsIURI         *aURI,
                              nsIChannel     *aChannel,
                              nsCookieAccess *aResult)
{
#ifdef MOZ_MAIL_NEWS
  // disable cookies in mailnews if user's prefs say so
  if (mCookiesDisabledForMailNews) {
    //
    // try to examine the "app type" of the docshell owning this request.  if
    // we find a docshell in the heirarchy of type APP_TYPE_MAIL, then assume
    // this URI is being loaded from within mailnews.
    //
    // XXX this is a pretty ugly hack at the moment since cookies really
    // shouldn't have to talk to the docshell directly.  ultimately, we want
    // to talk to some more generic interface, which the docshell would also
    // implement.  but, the basic mechanism here of leveraging the channel's
    // (or loadgroup's) notification callbacks attribute seems ideal as it
    // avoids the problem of having to modify all places in the code which
    // kick off network requests.
    //
    PRUint32 appType = nsIDocShell::APP_TYPE_UNKNOWN;
    if (aChannel) {
      nsCOMPtr<nsIDocShellTreeItem> item, parent;
      NS_QueryNotificationCallbacks(aChannel, parent);
      if (parent) {
        do {
            item = parent;
            nsCOMPtr<nsIDocShell> docshell = do_QueryInterface(item);
            if (docshell)
              docshell->GetAppType(&appType);
        } while (appType != nsIDocShell::APP_TYPE_MAIL &&
                 NS_SUCCEEDED(item->GetParent(getter_AddRefs(parent))) &&
                 parent);
      }
    }
    if ((appType == nsIDocShell::APP_TYPE_MAIL) ||
        IsFromMailNews(aURI)) {
      *aResult = ACCESS_DENY;
      return NS_OK;
    }
  }
#endif // MOZ_MAIL_NEWS
  
  // finally, check with permission manager...
  nsresult rv = mPermMgr->TestPermission(aURI, kPermissionType, (PRUint32 *) aResult);
  if (NS_SUCCEEDED(rv)) {
    switch (*aResult) {
    // if we have one of the publicly-available values, just return it
    case nsIPermissionManager::UNKNOWN_ACTION: // ACCESS_DEFAULT
    case nsIPermissionManager::ALLOW_ACTION:   // ACCESS_ALLOW
    case nsIPermissionManager::DENY_ACTION:    // ACCESS_DENY
      break;

    // ACCESS_SESSION means the cookie can be accepted; the session 
    // downgrade will occur in CanSetCookie().
    case nsICookiePermission::ACCESS_SESSION:
      *aResult = ACCESS_ALLOW;
      break;

    // ack, an unknown type! just use the defaults.
    default:
      *aResult = ACCESS_DEFAULT;
    }
  }

  return rv;
}

NS_IMETHODIMP 
nsCookiePermission::CanSetCookie(nsIURI     *aURI,
                                 nsIChannel *aChannel,
                                 nsICookie2 *aCookie,
                                 PRBool     *aIsSession,
                                 PRInt64    *aExpiry,
                                 PRBool     *aResult)
{
  NS_ASSERTION(aURI, "null uri");

  *aResult = kDefaultPolicy;

  PRUint32 perm;
  mPermMgr->TestPermission(aURI, kPermissionType, &perm);
  switch (perm) {
  case nsICookiePermission::ACCESS_SESSION:
    *aIsSession = PR_TRUE;

  case nsIPermissionManager::ALLOW_ACTION: // ACCESS_ALLOW
    *aResult = PR_TRUE;
    break;

  case nsIPermissionManager::DENY_ACTION:  // ACCESS_DENY
    *aResult = PR_FALSE;
    break;

  default:
    // the permission manager has nothing to say about this cookie -
    // so, we apply the default prefs to it.
    NS_ASSERTION(perm == nsIPermissionManager::UNKNOWN_ACTION, "unknown permission");
    
    // now we need to figure out what type of accept policy we're dealing with
    // if we accept cookies normally, just bail and return
    if (mCookiesLifetimePolicy == ACCEPT_NORMALLY) {
      *aResult = PR_TRUE;
      return NS_OK;
    }
    
    // declare this here since it'll be used in all of the remaining cases
    PRInt64 currentTime = PR_Now() / PR_USEC_PER_SEC;
    PRInt64 delta = *aExpiry - currentTime;
    
    // check whether the user wants to be prompted
    if (mCookiesLifetimePolicy == ASK_BEFORE_ACCEPT) {
      // if it's a session cookie and the user wants to accept these 
      // without asking, just accept the cookie and return
      if (*aIsSession && mCookiesAlwaysAcceptSession) {
        *aResult = PR_TRUE;
        return NS_OK;
      }
      
      // default to rejecting, in case the prompting process fails
      *aResult = PR_FALSE;

      nsCAutoString hostPort;
      aURI->GetHostPort(hostPort);

      if (!aCookie) {
         return NS_ERROR_UNEXPECTED;
      }
      // If there is no host, use the scheme, and append "://",
      // to make sure it isn't a host or something.
      // This is done to make the dialog appear for javascript cookies from
      // file:// urls, and make the text on it not too weird. (bug 209689)
      if (hostPort.IsEmpty()) {
        aURI->GetScheme(hostPort);
        if (hostPort.IsEmpty()) {
          // still empty. Just return the default.
          return NS_OK;
        }
        hostPort = hostPort + NS_LITERAL_CSTRING("://");
      }

      // we don't cache the cookiePromptService - it's not used often, so not
      // worth the memory.
      nsresult rv;
      nsCOMPtr<nsICookiePromptService> cookiePromptService =
          do_GetService(NS_COOKIEPROMPTSERVICE_CONTRACTID, &rv);
      if (NS_FAILED(rv)) return rv;

      // try to get a nsIDOMWindow from the channel...
      nsCOMPtr<nsIDOMWindow> parent;
      if (aChannel)
        NS_QueryNotificationCallbacks(aChannel, parent);

      // get some useful information to present to the user:
      // whether a previous cookie already exists, and how many cookies this host
      // has set
      PRBool foundCookie = PR_FALSE;
      PRUint32 countFromHost;
      nsCOMPtr<nsICookieManager2> cookieManager = do_GetService(NS_COOKIEMANAGER_CONTRACTID, &rv);
      if (NS_SUCCEEDED(rv)) {
        nsCAutoString rawHost;
        aCookie->GetRawHost(rawHost);
        rv = cookieManager->CountCookiesFromHost(rawHost, &countFromHost);

        if (NS_SUCCEEDED(rv) && countFromHost > 0)
          rv = cookieManager->CookieExists(aCookie, &foundCookie);
      }
      if (NS_FAILED(rv)) return rv;

      // check if the cookie we're trying to set is already expired, and return;
      // but only if there's no previous cookie, because then we need to delete the previous
      // cookie. we need this check to avoid prompting the user for already-expired cookies.
      if (!foundCookie && !*aIsSession && delta <= 0) {
        // the cookie has already expired. accept it, and let the backend figure
        // out it's expired, so that we get correct logging & notifications.
        *aResult = PR_TRUE;
        return rv;
      }

      PRBool rememberDecision = PR_FALSE;
      rv = cookiePromptService->CookieDialog(parent, aCookie, hostPort, 
                                             countFromHost, foundCookie,
                                             &rememberDecision, aResult);
      if (NS_FAILED(rv)) return rv;
      
      if (*aResult == nsICookiePromptService::ACCEPT_SESSION_COOKIE)
        *aIsSession = PR_TRUE;

      if (rememberDecision) {
        switch (*aResult) {
          case nsICookiePromptService::DENY_COOKIE:
            mPermMgr->Add(aURI, kPermissionType, (PRUint32) nsIPermissionManager::DENY_ACTION);
            break;
          case nsICookiePromptService::ACCEPT_COOKIE:
            mPermMgr->Add(aURI, kPermissionType, (PRUint32) nsIPermissionManager::ALLOW_ACTION);
            break;
          case nsICookiePromptService::ACCEPT_SESSION_COOKIE:
            mPermMgr->Add(aURI, kPermissionType, nsICookiePermission::ACCESS_SESSION);
            break;
          default:
            break;
        }
      }
    } else {
      // we're not prompting, so we must be limiting the lifetime somehow
      // if it's a session cookie, we do nothing
      if (!*aIsSession && delta > 0) {
        if (mCookiesLifetimePolicy == ACCEPT_SESSION) {
          // limit lifetime to session
          *aIsSession = PR_TRUE;
        } else if (delta > mCookiesLifetimeSec) {
          // limit lifetime to specified time
          *aExpiry = currentTime + mCookiesLifetimeSec;
        }
      }
    }
  }

  return NS_OK;
}

NS_IMETHODIMP 
nsCookiePermission::GetOriginatingURI(nsIChannel  *aChannel,
                                      nsIURI     **aURI)
{
  /* to find the originating URI, we use the loadgroup of the channel to obtain
   * the docshell owning the load, and from there, we find the root content
   * docshell and its URI. there are several possible cases:
   *
   * 1) no channel. this will occur for plugins using the nsICookieStorage
   *    interface, since they have none to provide. other consumers should
   *    have a channel.
   *
   * 2) a channel, but no docshell. this can occur when the consumer kicking
   *    off the load doesn't provide one to the channel, and should be limited
   *    to loads of certain types of resources (e.g. favicons).
   *
   * 3) a non-content docshell. this occurs for loads kicked off from chrome,
   *    where no content docshell exists (favicons can also fall into this
   *    category).
   *
   * 4) a content docshell equal to the root content docshell, with channel
   *    loadflags LOAD_DOCUMENT_URI. this covers the case of a freshly kicked-
   *    off load (e.g. the user typing something in the location bar, or
   *    clicking on a bookmark), where the currentURI hasn't yet been set,
   *    and will be bogus. we return the channel URI in this case. note that
   *    we could also allow non-content docshells here, but that goes against
   *    the philosophy of having an audit trail back to a URI the user typed
   *    or clicked on.
   *
   * 5) a root content docshell. this covers most cases for an ordinary page
   *    load from the location bar, and will catch nested frames within
   *    a page, image loads, etc. we return the URI of the docshell's principal
   *    in this case.
   *
   */

  *aURI = nsnull;

  // case 1)
  if (!aChannel)
    return NS_ERROR_NULL_POINTER;

  nsLoadFlags flags;
  aChannel->GetLoadFlags(&flags);

  if (flags & flockIChannel::SELF_ORIGINATING) {
    aChannel->GetURI(aURI);
    if (!*aURI)
      return NS_ERROR_NULL_POINTER;

    return NS_OK;
  }

  // find the docshell and its root
  nsCOMPtr<nsIDocShellTreeItem> docshell, root;
  NS_QueryNotificationCallbacks(aChannel, docshell);
  if (docshell)
    docshell->GetSameTypeRootTreeItem(getter_AddRefs(root));

  PRInt32 type;
  if (root)
    root->GetItemType(&type);

  // cases 2) and 3)
  if (!root || type != nsIDocShellTreeItem::typeContent)
    return NS_ERROR_INVALID_ARG;

  // case 4)
  if (docshell == root) {
    if (flags & nsIChannel::LOAD_DOCUMENT_URI) {
      // get the channel URI - the docshell's will be bogus
      aChannel->GetURI(aURI);
      if (!*aURI)
        return NS_ERROR_NULL_POINTER;

      return NS_OK;
    }
  }

  // case 5) - get the originating URI from the docshell's principal
  nsCOMPtr<nsIWebNavigation> webnav = do_QueryInterface(root);
  if (webnav) {
    nsCOMPtr<nsIDOMDocument> doc;
    webnav->GetDocument(getter_AddRefs(doc));
    nsCOMPtr<nsINode> node = do_QueryInterface(doc);
    if (node)
      node->NodePrincipal()->GetURI(aURI);
  }

  if (!*aURI)
    return NS_ERROR_NULL_POINTER;

  // all done!
  return NS_OK;
}

NS_IMETHODIMP
nsCookiePermission::Observe(nsISupports     *aSubject,
                            const char      *aTopic,
                            const PRUnichar *aData)
{
  nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject);
  NS_ASSERTION(!nsCRT::strcmp(NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, aTopic),
               "unexpected topic - we only deal with pref changes!");

  if (prefBranch)
    PrefChanged(prefBranch, NS_LossyConvertUTF16toASCII(aData).get());
  return NS_OK;
}
