From 416968bb0a2eaa16fa96f3b3a599651fc3bb6df5 Mon Sep 17 00:00:00 2001 From: "darin%netscape.com" Date: Wed, 9 Jan 2002 04:18:48 +0000 Subject: [PATCH] fixes bug 112564 "Cache-Control: no-cache should not affect back/forward buttons" r=dougt, sr=brendan git-svn-id: svn://10.0.0.236/trunk@111645 18797224-902f-48f8-a5cc-f745e15eee43 --- mozilla/docshell/base/nsDocShell.cpp | 23 ++- .../protocol/http/public/nsIHttpChannel.idl | 12 ++ .../protocol/http/src/nsHttpChannel.cpp | 92 +++++----- .../protocol/http/src/nsHttpResponseHead.cpp | 161 +++++++++++++----- .../protocol/http/src/nsHttpResponseHead.h | 34 +++- 5 files changed, 222 insertions(+), 100 deletions(-) diff --git a/mozilla/docshell/base/nsDocShell.cpp b/mozilla/docshell/base/nsDocShell.cpp index 1b4a128e770..341bbc57b7e 100644 --- a/mozilla/docshell/base/nsDocShell.cpp +++ b/mozilla/docshell/base/nsDocShell.cpp @@ -5162,8 +5162,8 @@ nsDocShell::AddToSessionHistory(nsIURI * aURI, nsCOMPtr referrerURI; nsCOMPtr cacheKey; nsCOMPtr cacheToken; - PRPackedBool expired = PR_FALSE; - nsXPIDLCString val; + PRBool expired = PR_FALSE; + PRBool discardLayoutState = PR_FALSE; if (aChannel) { nsCOMPtr cacheChannel(do_QueryInterface(aChannel)); @@ -5183,7 +5183,16 @@ nsDocShell::AddToSessionHistory(nsIURI * aURI, if (httpChannel) { httpChannel->GetUploadStream(getter_AddRefs(inputStream)); httpChannel->GetReferrer(getter_AddRefs(referrerURI)); - httpChannel->GetResponseHeader("Cache-Control", getter_Copies(val)); + + // figure out if SH should be saving layout state (see bug 112564) + nsCOMPtr securityInfo; + PRBool noStore = PR_FALSE, noCache = PR_FALSE; + + httpChannel->GetSecurityInfo(getter_AddRefs(securityInfo)); + httpChannel->IsNoStoreResponse(&noStore); + httpChannel->IsNoCacheResponse(&noCache); + + discardLayoutState = noStore || (noCache && securityInfo); } } @@ -5199,15 +5208,15 @@ nsDocShell::AddToSessionHistory(nsIURI * aURI, * HistoryLayoutState. By default, SH will set this * flag to PR_TRUE and save HistoryLayoutState. */ - if (val && (PL_strcasestr(val, "no-store") || PL_strcasestr(val, "no-cache"))) { + if (discardLayoutState) { entry->SetSaveLayoutStateFlag(PR_FALSE); } if (cacheToken) { // Check if the page has expired from cache - nsCOMPtr cacheEntryDesc(do_QueryInterface(cacheToken)); - if (cacheEntryDesc) { + nsCOMPtr cacheEntryInfo(do_QueryInterface(cacheToken)); + if (cacheEntryInfo) { PRUint32 expTime; - cacheEntryDesc->GetExpirationTime(&expTime); + cacheEntryInfo->GetExpirationTime(&expTime); PRUint32 now = PRTimeToSeconds(PR_Now()); if (expTime <= now) expired = PR_TRUE; diff --git a/mozilla/netwerk/protocol/http/public/nsIHttpChannel.idl b/mozilla/netwerk/protocol/http/public/nsIHttpChannel.idl index b145ae17719..48a84b8ec13 100644 --- a/mozilla/netwerk/protocol/http/public/nsIHttpChannel.idl +++ b/mozilla/netwerk/protocol/http/public/nsIHttpChannel.idl @@ -79,6 +79,18 @@ interface nsIHttpChannel : nsIChannel void setResponseHeader(in string header, in string value); void visitResponseHeaders(in nsIHttpHeaderVisitor visitor); + /** + * True if the server sent a "Cache-control: no-store" response header. + */ + boolean isNoStoreResponse(); + + /** + * True if the server sent the equivalent of a "Cache-control: no-cache" + * response header. Other equivalent response headers include: "Pragma: + * no-cache" and "Expires" with a date-value in the past. + */ + boolean isNoCacheResponse(); + /** * Get the charset for the response, which may be NULL if not specified * by the server (ie. the Content-Type header may not specify a charset). diff --git a/mozilla/netwerk/protocol/http/src/nsHttpChannel.cpp b/mozilla/netwerk/protocol/http/src/nsHttpChannel.cpp index 7c90ddbfa5e..0eaaba205cf 100644 --- a/mozilla/netwerk/protocol/http/src/nsHttpChannel.cpp +++ b/mozilla/netwerk/protocol/http/src/nsHttpChannel.cpp @@ -742,7 +742,7 @@ nsHttpChannel::UpdateExpirationTime() NS_ENSURE_TRUE(mResponseHead, NS_ERROR_FAILURE); - if (!mResponseHead->MustRevalidate()) { + if (!mResponseHead->MustValidate()) { nsresult rv; PRUint32 freshnessLifetime, currentAge; @@ -841,33 +841,36 @@ nsHttpChannel::CheckCache() if (mLoadFlags & LOAD_FROM_CACHE) { LOG(("NOT validating based on LOAD_FROM_CACHE load flag\n")); doValidation = PR_FALSE; - goto end; } - // If the VALIDATE_ALWAYS flag is set, any cached data won't be used until // it's revalidated with the server. - if (mLoadFlags & VALIDATE_ALWAYS) { + else if (mLoadFlags & VALIDATE_ALWAYS) { LOG(("Validating based on VALIDATE_ALWAYS load flag\n")); doValidation = PR_TRUE; - goto end; } - - // check revalidation is strictly required. - if (mCachedResponseHead->MustRevalidate()) { + // Even if the VALIDATE_NEVER flag is set, there are still some cases in + // which we must validate the cached response with the server. + else if (mLoadFlags & VALIDATE_NEVER) { + LOG(("VALIDATE_NEVER set\n")); + // if no-store or if no-cache and ssl, validate cached response (see + // bug 112564 for an explanation of this logic) + if (mCachedResponseHead->NoStore() || + (mCachedResponseHead->NoCache() && mConnectionInfo->UsingSSL())) { + LOG(("Validating based on (no-store || (no-cache && ssl)) logic\n")); + doValidation = PR_TRUE; + } + else { + LOG(("NOT validating based on VALIDATE_NEVER load flag\n")); + doValidation = PR_FALSE; + } + } + // check if validation is strictly required... + else if (mCachedResponseHead->MustValidate()) { + LOG(("Validating based on MustValidate() returning TRUE\n")); doValidation = PR_TRUE; - goto end; } - - // delay checking this flag until we've verified that the response headers - // do not require mandatory revalidation. - if (mLoadFlags & VALIDATE_NEVER) { - LOG(("Not validating based on VALIDATE_NEVER flag\n")); - doValidation = PR_FALSE; - goto end; - } - // Check if the cache entry has expired... - { + else { PRUint32 time = 0; // a temporary variable for storing time values... rv = mCacheEntry->GetExpirationTime(&time); @@ -875,6 +878,8 @@ nsHttpChannel::CheckCache() if (NowInSeconds() <= time) doValidation = PR_FALSE; + else if (mCachedResponseHead->MustValidateIfExpired()) + doValidation = PR_TRUE; else if (mLoadFlags & VALIDATE_ONCE_PER_SESSION) { // If the cached response does not include expiration infor- // mation, then we must validate the response, despite whether @@ -894,7 +899,6 @@ nsHttpChannel::CheckCache() // has been accessed in this session, and validate if so. doValidation = (nsHttpHandler::get()->SessionStartTime() > time); } - } else doValidation = PR_TRUE; @@ -902,10 +906,11 @@ nsHttpChannel::CheckCache() LOG(("%salidating based on expiration time\n", doValidation ? "V" : "Not v")); } -end: mCachedContentIsValid = !doValidation; - if (doValidation) { + // add validation headers unless the cached response is marked no-store... + // this'll force no-store content to be refetched each time from the server. + if (doValidation && !mCachedResponseHead->NoStore()) { const char *val; // Add If-Modified-Since header if a Last-Modified was given val = mCachedResponseHead->PeekHeader(nsHttp::Last_Modified); @@ -1016,7 +1021,6 @@ nsHttpChannel::CloseCacheEntry(nsresult status) nsresult nsHttpChannel::InitCacheEntry() { - const char *val; nsresult rv; NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_UNEXPECTED); @@ -1029,16 +1033,10 @@ nsHttpChannel::InitCacheEntry() LOG(("nsHttpChannel::InitCacheEntry [this=%x entry=%x]\n", this, mCacheEntry.get())); - // XXX blow away any existing cache meta data - // The no-store directive within the 'Cache-Control:' header indicates - // that we should not store the response in the cache. - val = mResponseHead->PeekHeader(nsHttp::Cache_Control); - if (val && PL_strcasestr(val, "no-store")) { - LOG(("Inhibiting caching because of \"%s\"\n", val)); - CloseCacheEntry(NS_ERROR_ABORT); - return NS_OK; - } + // that we must not store the response in a persistent cache. + if (mResponseHead->NoStore()) + mLoadFlags |= INHIBIT_PERSISTENT_CACHING; // For HTTPS transactions, the storage policy will already be IN_MEMORY. // We are concerned instead about load attributes which may have changed. @@ -1077,16 +1075,6 @@ nsHttpChannel::FinalizeCacheEntry() LOG(("nsHttpChannel::FinalizeCacheEntry [this=%x]\n", this)); if (mResponseHead && mResponseHeadersModified) { - // The no-store directive within the 'Cache-Control:' header indicates - // that we should not store the response in the cache. - // XXX this should probably be done from within SetResponseHeader. - const char *val = mResponseHead->PeekHeader(nsHttp::Cache_Control); - if (val && PL_strcasestr(val, "no-store")) { - LOG(("Dooming cache entry because of \"%s\"\n", val)); - mCacheEntry->Doom(); - return NS_OK; - } - // Set the expiration time for this cache entry nsresult rv = UpdateExpirationTime(); if (NS_FAILED(rv)) return rv; @@ -2281,6 +2269,26 @@ nsHttpChannel::VisitResponseHeaders(nsIHttpHeaderVisitor *visitor) return mResponseHead->Headers().VisitHeaders(visitor); } +NS_IMETHODIMP +nsHttpChannel::IsNoStoreResponse(PRBool *value) +{ + if (!mResponseHead) + return NS_ERROR_NOT_AVAILABLE; + *value = mResponseHead->NoStore(); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::IsNoCacheResponse(PRBool *value) +{ + if (!mResponseHead) + return NS_ERROR_NOT_AVAILABLE; + *value = mResponseHead->NoCache(); + if (!*value) + *value = mResponseHead->ExpiresInPast(); + return NS_OK; +} + NS_IMETHODIMP nsHttpChannel::GetCharset(char **value) { diff --git a/mozilla/netwerk/protocol/http/src/nsHttpResponseHead.cpp b/mozilla/netwerk/protocol/http/src/nsHttpResponseHead.cpp index 33f554835a4..65cbed8fc76 100644 --- a/mozilla/netwerk/protocol/http/src/nsHttpResponseHead.cpp +++ b/mozilla/netwerk/protocol/http/src/nsHttpResponseHead.cpp @@ -31,6 +31,21 @@ // nsHttpResponseHead //----------------------------------------------------------------------------- +nsresult +nsHttpResponseHead::SetHeader(nsHttpAtom hdr, const char *val) +{ + nsresult rv = mHeaders.SetHeader(hdr, val); + if (NS_FAILED(rv)) return rv; + + // response to changes in these headers + if (hdr == nsHttp::Cache_Control) + ParseCacheControl(val); + else if (hdr == nsHttp::Pragma) + ParsePragma(val); + + return NS_OK; +} + void nsHttpResponseHead::SetContentLength(PRInt32 len) { @@ -143,7 +158,7 @@ nsHttpResponseHead::ParseStatusLine(char *line) if ((mVersion == NS_HTTP_VERSION_0_9) || !(line = PL_strchr(line, ' '))) { mStatus = 200; - mStatusText.Adopt(nsCRT::strdup("OK")); + mStatusText = nsCRT::strdup("OK"); } else { // Status-Code @@ -156,14 +171,14 @@ nsHttpResponseHead::ParseStatusLine(char *line) // Reason-Phrase is whatever is remaining of the line if (!(line = PL_strchr(line, ' '))) { LOG(("mal-formed response status line; assuming statusText = 'OK'\n")); - mStatusText.Adopt(nsCRT::strdup("OK")); + mStatusText = nsCRT::strdup("OK"); } else - mStatusText.Adopt(nsCRT::strdup(++line)); + mStatusText = nsCRT::strdup(++line); } LOG(("Have status line [version=%u status=%u statusText=%s]\n", - PRUintn(mVersion), PRUintn(mStatus), mStatusText.get())); + PRUintn(mVersion), PRUintn(mStatus), mStatusText)); } void @@ -179,6 +194,10 @@ nsHttpResponseHead::ParseHeaderLine(char *line) mContentLength = atoi(val); else if (hdr == nsHttp::Content_Type) ParseContentType(val); + else if (hdr == nsHttp::Cache_Control) + ParseCacheControl(val); + else if (hdr == nsHttp::Pragma) + ParsePragma(val); } // From section 13.2.3 of RFC2616, we compute the current age of a cached @@ -280,47 +299,33 @@ nsHttpResponseHead::ComputeFreshnessLifetime(PRUint32 *result) } PRBool -nsHttpResponseHead::MustRevalidate() +nsHttpResponseHead::MustValidate() { const char *val; - LOG(("nsHttpResponseHead::MustRevalidate ??\n")); + LOG(("nsHttpResponseHead::MustValidate ??\n")); - // If the must-revalidate directive is present in the cached response, data - // must always be revalidated with the server, even if the user has - // configured validation to be turned off (See RFC 2616, section 14.9.4). - val = PeekHeader(nsHttp::Cache_Control); - if (val && PL_strcasestr(val, "must-revalidate")) { - LOG(("Must revalidate based on \"%s\" header\n", val)); + // The no-cache response header indicates that we must validate this + // cached response before reusing. + if (NoCache()) { + LOG(("Must validate since response contains 'no-cache' header\n")); return PR_TRUE; } - // The no-cache directive within the 'Cache-Control:' header indicates - // that we must validate this cached response before reusing. - if (val && PL_strcasestr(val, "no-cache")) { - LOG(("Must revalidate based on \"%s\" header\n", val)); - return PR_TRUE; - } - // XXX we are not quite handling no-cache correctly in this case. We really - // should check for field-names and only force validation if they match - // existing response headers. See RFC2616 section 14.9.1 for details. - // Although 'Pragma:no-cache' is not a standard HTTP response header (it's - // a request header), caching is inhibited when this header is present so - // as to match existing Navigator behavior. - val = PeekHeader(nsHttp::Pragma); - if (val && PL_strcasestr(val, "no-cache")) { - LOG(("Must revalidate based on \"%s\" header\n", val)); + // Likewise, if the response is no-store, then we must validate this + // cached response before reusing. NOTE: it may seem odd that a no-store + // response may be cached, but indeed all responses are cached in order + // to support File->SaveAs, View->PageSource, and other browser features. + if (NoStore()) { + LOG(("Must validate since response contains 'no-store' header\n")); return PR_TRUE; } // Compare the Expires header to the Date header. If the server sent an // Expires header with a timestamp in the past, then we must validate this // cached response before reusing. - PRUint32 expiresVal, dateVal; - if (NS_SUCCEEDED(GetExpiresValue(&expiresVal)) && - NS_SUCCEEDED(GetDateValue(&dateVal)) && - expiresVal < dateVal) { - LOG(("Must revalidate since Expires < Date\n")); + if (ExpiresInPast()) { + LOG(("Must validate since Expires < Date\n")); return PR_TRUE; } @@ -339,14 +344,36 @@ nsHttpResponseHead::MustRevalidate() if (val && (PL_strstr(val, "*") || PL_strcasestr(val, "accept-charset") || PL_strcasestr(val, "accept-language"))) { - LOG(("Must revalidate based on \"%s\" header\n", val)); + LOG(("Must validate based on \"%s\" header\n", val)); return PR_TRUE; } - LOG(("no mandatory revalidation requirement\n")); + LOG(("no mandatory validation requirement\n")); return PR_FALSE; } +PRBool +nsHttpResponseHead::MustValidateIfExpired() +{ + // according to RFC2616, section 14.9.4: + // + // When the must-revalidate directive is present in a response received by a + // cache, that cache MUST NOT use the entry after it becomes stale to respond to + // a subsequent request without first revalidating it with the origin server. + // + const char *val = PeekHeader(nsHttp::Cache_Control); + return val && PL_strcasestr(val, "must-revalidate"); +} + +PRBool +nsHttpResponseHead::ExpiresInPast() +{ + PRUint32 expiresVal, dateVal; + return NS_SUCCEEDED(GetExpiresValue(&expiresVal)) && + NS_SUCCEEDED(GetDateValue(&dateVal)) && + expiresVal < dateVal; +} + nsresult nsHttpResponseHead::UpdateHeaders(nsHttpHeaderArray &headers) { @@ -410,10 +437,13 @@ nsHttpResponseHead::Reset() mVersion = NS_HTTP_VERSION_1_1; mStatus = 200; - mStatusText.Adopt(0); mContentLength = -1; - mContentType.Adopt(0); - mContentCharset.Adopt(0); + mCacheControlNoStore = PR_FALSE; + mCacheControlNoCache = PR_FALSE; + mPragmaNoCache = PR_FALSE; + CRTFREEIF(mStatusText); + CRTFREEIF(mContentType); + CRTFREEIF(mContentCharset); } nsresult @@ -510,14 +540,19 @@ nsHttpResponseHead::ParseVersion(const char *str) // This code is duplicated in nsMultiMixedConv.cpp. If you change it // here, change it there, too! -nsresult +void nsHttpResponseHead::ParseContentType(char *type) { LOG(("nsHttpResponseHead::ParseContentType [type=%s]\n", type)); // don't bother with an empty content-type header - bug 83465 if (!*type) - return NS_OK; + return; + + // a response could have multiple content type headers... we'll honor + // the last one. + CRTFREEIF(mContentCharset); + CRTFREEIF(mContentType); // we don't care about comments (although they are invalid here) char *p = PL_strchr(type, '('); @@ -541,7 +576,7 @@ nsHttpResponseHead::ParseContentType(char *type) } while ((*p3 == ' ') || (*p3 == '\t')); *++p3 = 0; // overwrite first char after the charset field - mContentCharset.Adopt(nsCRT::strdup(p2)); + mContentCharset = nsCRT::strdup(p2); } } else @@ -556,7 +591,47 @@ nsHttpResponseHead::ParseContentType(char *type) while (--p >= type) *p = nsCRT::ToLower(*p); - mContentType.Adopt(nsCRT::strdup(type)); - - return NS_OK; + mContentType = nsCRT::strdup(type); +} + +void +nsHttpResponseHead::ParseCacheControl(const char *val) +{ + if (!val) { + // clear no-cache flag + mCacheControlNoCache = PR_FALSE; + return; + } + else if (!*val) + return; + + const char *s = val; + + // search header value for occurance(s) of "no-cache" but ignore + // occurance(s) of "no-cache=blah" + while (s = PL_strcasestr(s, "no-cache")) { + s += (sizeof("no-cache") - 1); + if (*s != '=') + mCacheControlNoCache = PR_TRUE; + } + + // search header value for occurance of "no-store" + if (PL_strcasestr(val, "no-store")) + mCacheControlNoStore = PR_TRUE; +} + +void +nsHttpResponseHead::ParsePragma(const char *val) +{ + if (!val) { + // clear no-cache flag + mPragmaNoCache = PR_FALSE; + return; + } + + // Although 'Pragma:no-cache' is not a standard HTTP response header (it's + // a request header), caching is inhibited when this header is present so + // as to match existing Navigator behavior. + if (*val && !PL_strcasestr(val, "no-cache")) + mPragmaNoCache = PR_TRUE; } diff --git a/mozilla/netwerk/protocol/http/src/nsHttpResponseHead.h b/mozilla/netwerk/protocol/http/src/nsHttpResponseHead.h index 3c031ce1d9d..346736359cb 100644 --- a/mozilla/netwerk/protocol/http/src/nsHttpResponseHead.h +++ b/mozilla/netwerk/protocol/http/src/nsHttpResponseHead.h @@ -38,7 +38,13 @@ class nsHttpResponseHead public: nsHttpResponseHead() : mVersion(NS_HTTP_VERSION_1_1) , mStatus(200) - , mContentLength(-1) {} + , mStatusText(nsnull) + , mContentLength(-1) + , mContentType(nsnull) + , mContentCharset(nsnull) + , mCacheControlNoStore(PR_FALSE) + , mCacheControlNoCache(PR_FALSE) + , mPragmaNoCache(PR_FALSE) {} ~nsHttpResponseHead() {} nsHttpHeaderArray &Headers() { return mHeaders; } @@ -48,13 +54,15 @@ public: PRInt32 ContentLength() { return mContentLength; } const char *ContentType() { return mContentType; } const char *ContentCharset() { return mContentCharset; } + PRBool NoStore() { return mCacheControlNoStore; } + PRBool NoCache() { return (mCacheControlNoCache || mPragmaNoCache); } const char *PeekHeader(nsHttpAtom h) { return mHeaders.PeekHeader(h); } - nsresult SetHeader(nsHttpAtom h, const char *v) { return mHeaders.SetHeader(h, v); } + nsresult SetHeader(nsHttpAtom h, const char *v); nsresult GetHeader(nsHttpAtom h, char **v) { return mHeaders.GetHeader(h, v); } void ClearHeaders() { mHeaders.Clear(); } - void SetContentType(const char *s) { mContentType.Adopt(s ? nsCRT::strdup(s) : 0); } + void SetContentType(const char *s) { CRTFREEIF(mContentType); mContentType = (s ? nsCRT::strdup(s) : 0); } void SetContentLength(PRInt32); // write out the response status line and headers as a single text block, @@ -75,7 +83,12 @@ public: // cache validation support methods nsresult ComputeFreshnessLifetime(PRUint32 *); nsresult ComputeCurrentAge(PRUint32 now, PRUint32 requestTime, PRUint32 *result); - PRBool MustRevalidate(); + PRBool MustValidate(); + PRBool MustValidateIfExpired(); + + // returns true if the Expires header has a value in the past relative to the + // value of the Date header. + PRBool ExpiresInPast(); // update headers... nsresult UpdateHeaders(nsHttpHeaderArray &headers); @@ -93,16 +106,21 @@ public: private: void ParseVersion(const char *); - nsresult ParseContentType(char *); + void ParseContentType(char *); + void ParseCacheControl(const char *); + void ParsePragma(const char *); private: nsHttpHeaderArray mHeaders; nsHttpVersion mVersion; PRUint16 mStatus; - nsXPIDLCString mStatusText; + char *mStatusText; PRInt32 mContentLength; - nsXPIDLCString mContentType; - nsXPIDLCString mContentCharset; + char *mContentType; + char *mContentCharset; + PRPackedBool mCacheControlNoStore; + PRPackedBool mCacheControlNoCache; + PRPackedBool mPragmaNoCache; }; #endif // nsHttpResponseHead_h__