/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/chrome/RegistryMessageUtils.h"

#include "nsResProtocolHandler.h"
#include "nsIURL.h"
#include "nsIIOService.h"
#include "nsIServiceManager.h"
#include "nsILocalFile.h"
#include "prenv.h"
#include "prmem.h"
#include "prprf.h"
#include "nsXPIDLString.h"
#include "nsIFile.h"
#include "nsDirectoryServiceDefs.h"
#include "nsNetUtil.h"
#include "nsURLHelper.h"
#include "nsEscape.h"

#include "mozilla/Omnijar.h"

static NS_DEFINE_CID(kResURLCID, NS_RESURL_CID);

static nsResProtocolHandler *gResHandler = nsnull;

#if defined(PR_LOGGING)
//
// Log module for Resource Protocol logging...
//
// To enable logging (see prlog.h for full details):
//
//    set NSPR_LOG_MODULES=nsResProtocol:5
//    set NSPR_LOG_FILE=log.txt
//
// this enables PR_LOG_ALWAYS level information and places all output in
// the file log.txt
//
static PRLogModuleInfo *gResLog;
#endif

#define kAPP           NS_LITERAL_CSTRING("app")
#define kGRE           NS_LITERAL_CSTRING("gre")

//----------------------------------------------------------------------------
// nsResURL : overrides nsStandardURL::GetFile to provide nsIFile resolution
//----------------------------------------------------------------------------

nsresult
nsResURL::EnsureFile()
{
    nsresult rv;

    NS_ENSURE_TRUE(gResHandler, NS_ERROR_NOT_AVAILABLE);

    nsCAutoString spec;
    rv = gResHandler->ResolveURI(this, spec);
    if (NS_FAILED(rv))
        return rv;

    nsCAutoString scheme;
    rv = net_ExtractURLScheme(spec, nsnull, nsnull, &scheme);
    if (NS_FAILED(rv))
        return rv;

    // Bug 585869:
    // In most cases, the scheme is jar if it's not file.
    // Regardless, net_GetFileFromURLSpec should be avoided
    // when the scheme isn't file.
    if (!scheme.Equals(NS_LITERAL_CSTRING("file")))
        return NS_ERROR_NO_INTERFACE;

    rv = net_GetFileFromURLSpec(spec, getter_AddRefs(mFile));
#ifdef DEBUG_bsmedberg
    if (NS_SUCCEEDED(rv)) {
        bool exists = true;
        mFile->Exists(&exists);
        if (!exists) {
            printf("resource %s doesn't exist!\n", spec.get());
        }
    }
#endif

    return rv;
}

/* virtual */ nsStandardURL*
nsResURL::StartClone()
{
    nsResURL *clone = new nsResURL();
    return clone;
}

NS_IMETHODIMP 
nsResURL::GetClassIDNoAlloc(nsCID *aClassIDNoAlloc)
{
    *aClassIDNoAlloc = kResURLCID;
    return NS_OK;
}

//----------------------------------------------------------------------------
// nsResProtocolHandler <public>
//----------------------------------------------------------------------------

nsResProtocolHandler::nsResProtocolHandler()
{
#if defined(PR_LOGGING)
    gResLog = PR_NewLogModule("nsResProtocol");
#endif

    NS_ASSERTION(!gResHandler, "res handler already created!");
    gResHandler = this;
}

nsResProtocolHandler::~nsResProtocolHandler()
{
    gResHandler = nsnull;
}

nsresult
nsResProtocolHandler::Init()
{
    mSubstitutions.Init(32);

    nsresult rv;

    mIOService = do_GetIOService(&rv);
    NS_ENSURE_SUCCESS(rv, rv);

    nsCAutoString appURI, greURI;
    rv = mozilla::Omnijar::GetURIString(mozilla::Omnijar::APP, appURI);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = mozilla::Omnijar::GetURIString(mozilla::Omnijar::GRE, greURI);
    NS_ENSURE_SUCCESS(rv, rv);

    //
    // make resource:/// point to the application directory or omnijar
    //
    nsCOMPtr<nsIURI> uri;
    rv = NS_NewURI(getter_AddRefs(uri), appURI.Length() ? appURI : greURI);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = SetSubstitution(EmptyCString(), uri);
    NS_ENSURE_SUCCESS(rv, rv);

    //
    // make resource://app/ point to the application directory or omnijar
    //
    rv = SetSubstitution(kAPP, uri);
    NS_ENSURE_SUCCESS(rv, rv);

    //
    // make resource://gre/ point to the GRE directory
    //
    if (appURI.Length()) { // We already have greURI in uri if appURI.Length() is 0.
        rv = NS_NewURI(getter_AddRefs(uri), greURI);
        NS_ENSURE_SUCCESS(rv, rv);
    }

    rv = SetSubstitution(kGRE, uri);
    NS_ENSURE_SUCCESS(rv, rv);

    //XXXbsmedberg Neil wants a resource://pchrome/ for the profile chrome dir...
    // but once I finish multiple chrome registration I'm not sure that it is needed

    // XXX dveditz: resource://pchrome/ defeats profile directory salting
    // if web content can load it. Tread carefully.

    return rv;
}

static PLDHashOperator
EnumerateSubstitution(const nsACString& aKey,
                      nsIURI* aURI,
                      void* aArg)
{
    nsTArray<ResourceMapping>* resources =
            static_cast<nsTArray<ResourceMapping>*>(aArg);
    SerializedURI uri;
    if (aURI) {
        aURI->GetSpec(uri.spec);
        aURI->GetOriginCharset(uri.charset);
    }

    ResourceMapping resource = {
        nsCString(aKey), uri
    };
    resources->AppendElement(resource);
    return (PLDHashOperator)PL_DHASH_NEXT;
}

void
nsResProtocolHandler::CollectSubstitutions(InfallibleTArray<ResourceMapping>& aResources)
{
    mSubstitutions.EnumerateRead(&EnumerateSubstitution, &aResources);
}

//----------------------------------------------------------------------------
// nsResProtocolHandler::nsISupports
//----------------------------------------------------------------------------

NS_IMPL_THREADSAFE_ISUPPORTS3(nsResProtocolHandler,
                              nsIResProtocolHandler,
                              nsIProtocolHandler,
                              nsISupportsWeakReference)

//----------------------------------------------------------------------------
// nsResProtocolHandler::nsIProtocolHandler
//----------------------------------------------------------------------------

NS_IMETHODIMP
nsResProtocolHandler::GetScheme(nsACString &result)
{
    result.AssignLiteral("resource");
    return NS_OK;
}

NS_IMETHODIMP
nsResProtocolHandler::GetDefaultPort(PRInt32 *result)
{
    *result = -1;        // no port for res: URLs
    return NS_OK;
}

NS_IMETHODIMP
nsResProtocolHandler::GetProtocolFlags(PRUint32 *result)
{
    // XXXbz Is this really true for all resource: URIs?  Could we
    // somehow give different flags to some of them?
    *result = URI_STD | URI_IS_UI_RESOURCE | URI_IS_LOCAL_RESOURCE;
    return NS_OK;
}

NS_IMETHODIMP
nsResProtocolHandler::NewURI(const nsACString &aSpec,
                             const char *aCharset,
                             nsIURI *aBaseURI,
                             nsIURI **result)
{
    nsresult rv;

    nsResURL *resURL = new nsResURL();
    if (!resURL)
        return NS_ERROR_OUT_OF_MEMORY;
    NS_ADDREF(resURL);

    // unescape any %2f and %2e to make sure nsStandardURL coalesces them.
    // Later net_GetFileFromURLSpec() will do a full unescape and we want to
    // treat them the same way the file system will. (bugs 380994, 394075)
    nsCAutoString spec;
    const char *src = aSpec.BeginReading();
    const char *end = aSpec.EndReading();
    const char *last = src;

    spec.SetCapacity(aSpec.Length()+1);
    for ( ; src < end; ++src) {
        if (*src == '%' && (src < end-2) && *(src+1) == '2') {
           char ch = '\0';
           if (*(src+2) == 'f' || *(src+2) == 'F')
             ch = '/';
           else if (*(src+2) == 'e' || *(src+2) == 'E')
             ch = '.';

           if (ch) {
             if (last < src)
               spec.Append(last, src-last);
             spec.Append(ch);
             src += 2;
             last = src+1; // src will be incremented by the loop
           }
        }
    }
    if (last < src)
      spec.Append(last, src-last);

    rv = resURL->Init(nsIStandardURL::URLTYPE_STANDARD, -1, spec, aCharset, aBaseURI);
    if (NS_SUCCEEDED(rv))
        rv = CallQueryInterface(resURL, result);
    NS_RELEASE(resURL);
    return rv;
}

NS_IMETHODIMP
nsResProtocolHandler::NewChannel(nsIURI* uri, nsIChannel* *result)
{
    NS_ENSURE_ARG_POINTER(uri);
    nsresult rv;
    nsCAutoString spec;

    rv = ResolveURI(uri, spec);
    if (NS_FAILED(rv)) return rv;

    rv = mIOService->NewChannel(spec, nsnull, nsnull, result);
    if (NS_FAILED(rv)) return rv;

    nsLoadFlags loadFlags = 0;
    (*result)->GetLoadFlags(&loadFlags);
    (*result)->SetLoadFlags(loadFlags & ~nsIChannel::LOAD_REPLACE);
    return (*result)->SetOriginalURI(uri);
}

NS_IMETHODIMP 
nsResProtocolHandler::AllowPort(PRInt32 port, const char *scheme, bool *_retval)
{
    // don't override anything.  
    *_retval = false;
    return NS_OK;
}

//----------------------------------------------------------------------------
// nsResProtocolHandler::nsIResProtocolHandler
//----------------------------------------------------------------------------

NS_IMETHODIMP
nsResProtocolHandler::SetSubstitution(const nsACString& root, nsIURI *baseURI)
{
    if (!baseURI) {
        mSubstitutions.Remove(root);
        return NS_OK;
    }

    // If baseURI isn't a resource URI, we can set the substitution immediately.
    nsCAutoString scheme;
    nsresult rv = baseURI->GetScheme(scheme);
    NS_ENSURE_SUCCESS(rv, rv);
    if (!scheme.Equals(NS_LITERAL_CSTRING("resource"))) {
        mSubstitutions.Put(root, baseURI);
        return NS_OK;
    }

    // baseURI is a resource URI, let's resolve it first.
    nsCAutoString newBase;
    rv = ResolveURI(baseURI, newBase);
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsIURI> newBaseURI;
    rv = mIOService->NewURI(newBase, nsnull, nsnull,
                            getter_AddRefs(newBaseURI));
    NS_ENSURE_SUCCESS(rv, rv);

    mSubstitutions.Put(root, newBaseURI);
    return NS_OK;
}

NS_IMETHODIMP
nsResProtocolHandler::GetSubstitution(const nsACString& root, nsIURI **result)
{
    NS_ENSURE_ARG_POINTER(result);

    if (mSubstitutions.Get(root, result))
        return NS_OK;

    // try invoking the directory service for "resource:root"

    nsCAutoString key;
    key.AssignLiteral("resource:");
    key.Append(root);

    nsCOMPtr<nsIFile> file;
    nsresult rv = NS_GetSpecialDirectory(key.get(), getter_AddRefs(file));
    if (NS_FAILED(rv))
        return NS_ERROR_NOT_AVAILABLE;
        
    rv = mIOService->NewFileURI(file, result);
    if (NS_FAILED(rv))
        return NS_ERROR_NOT_AVAILABLE;

    return NS_OK;
}

NS_IMETHODIMP
nsResProtocolHandler::HasSubstitution(const nsACString& root, bool *result)
{
    NS_ENSURE_ARG_POINTER(result);

    *result = mSubstitutions.Get(root, nsnull);
    return NS_OK;
}

NS_IMETHODIMP
nsResProtocolHandler::ResolveURI(nsIURI *uri, nsACString &result)
{
    nsresult rv;

    nsCAutoString host;
    nsCAutoString path;

    rv = uri->GetAsciiHost(host);
    if (NS_FAILED(rv)) return rv;

    rv = uri->GetPath(path);
    if (NS_FAILED(rv)) return rv;

    // Unescape the path so we can perform some checks on it.
    nsCAutoString unescapedPath(path);
    NS_UnescapeURL(unescapedPath);

    // Don't misinterpret the filepath as an absolute URI.
    if (unescapedPath.FindChar(':') != -1)
        return NS_ERROR_MALFORMED_URI;

    if (unescapedPath.FindChar('\\') != -1)
        return NS_ERROR_MALFORMED_URI;

    const char *p = path.get() + 1; // path always starts with a slash
    NS_ASSERTION(*(p-1) == '/', "Path did not begin with a slash!");

    if (*p == '/')
        return NS_ERROR_MALFORMED_URI;

    nsCOMPtr<nsIURI> baseURI;
    rv = GetSubstitution(host, getter_AddRefs(baseURI));
    if (NS_FAILED(rv)) return rv;

    rv = baseURI->Resolve(nsDependentCString(p, path.Length()-1), result);

#if defined(PR_LOGGING)
    if (PR_LOG_TEST(gResLog, PR_LOG_DEBUG)) {
        nsCAutoString spec;
        uri->GetAsciiSpec(spec);
        PR_LOG(gResLog, PR_LOG_DEBUG,
               ("%s\n -> %s\n", spec.get(), PromiseFlatCString(result).get()));
    }
#endif
    return rv;
}
