Bug 209275 - Links' hrefs should be updated when a <base>'s href changes. r=bzbarsky
authorJustin Lebar <jlebar@mozilla.com>
Fri, 25 Sep 2009 12:50:26 -0400
changeset 33055 018f433b13f5f9c6367fce2d385bab2e26b06313
parent 33054 6282a369f3ec8c51b0054cbed399b6ad4e4da89a
child 33056 b8e3560605cdb87bbde0a6aa831bea4fc122ea2a
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbzbarsky
bugs209275
milestone1.9.3a1pre
Bug 209275 - Links' hrefs should be updated when a <base>'s href changes. r=bzbarsky
content/base/public/nsIContent.h
content/base/public/nsIDocument.h
content/base/src/nsDocument.cpp
content/base/src/nsDocument.h
content/html/content/src/nsGenericHTMLElement.cpp
content/html/content/src/nsHTMLAnchorElement.cpp
content/html/content/src/nsHTMLSharedElement.cpp
content/html/content/test/Makefile.in
content/html/content/test/file_bug209275_1.html
content/html/content/test/file_bug209275_2.html
content/html/content/test/file_bug209275_3.html
content/html/content/test/test_bug209275.xhtml
--- a/content/base/public/nsIContent.h
+++ b/content/base/public/nsIContent.h
@@ -65,20 +65,20 @@ class nsIDOMCSSStyleDeclaration;
 enum nsLinkState {
   eLinkState_Unknown    = 0,
   eLinkState_Unvisited  = 1,
   eLinkState_Visited    = 2,
   eLinkState_NotLink    = 3
 };
 
 // IID for the nsIContent interface
-// b877753c-316a-422d-9aec-a1d0cf0928b0
+// d510382f-f5eb-48bb-9ad9-b3dc4806faaf
 #define NS_ICONTENT_IID       \
-{ 0xb877753c, 0x316a, 0x422d, \
-  { 0x9a, 0xec, 0xa1, 0xd0, 0xcf, 0x09, 0x28, 0xb0 } }
+{ 0xd510382f, 0xf5eb, 0x48bb, \
+  { 0x9a, 0xd9, 0xb3, 0xdc, 0x48, 0x06, 0xfa, 0xaf } }
 
 /**
  * A node of content in a document's content model. This interface
  * is supported by all content objects.
  */
 class nsIContent : public nsINode {
 public:
 #ifdef MOZILLA_INTERNAL_API
@@ -602,16 +602,27 @@ public:
    *
    * @note The out param, aURI, is guaranteed to be set to a non-null pointer
    *   when the return value is PR_TRUE.
    *
    * XXXjwatt: IMO IsInteractiveLink would be a better name.
    */
   virtual PRBool IsLink(nsIURI** aURI) const = 0;
 
+   /**
+   * If the implementing element is a link, calling this method forces it to
+   * clear its cached href, if it has one.
+   *
+   * This function does not notify the document that it may need to restyle the
+   * link.
+   */
+  virtual void DropCachedHref()
+  {
+  }
+
   /**
    * Get the cached state of the link.  If the state is unknown, 
    * return eLinkState_Unknown.
    *
    * @return The cached link state of the link.
    */
   virtual nsLinkState GetLinkState() const
   {
--- a/content/base/public/nsIDocument.h
+++ b/content/base/public/nsIDocument.h
@@ -100,18 +100,18 @@ class nsBindingManager;
 class nsIDOMNodeList;
 class mozAutoSubtreeModified;
 struct JSObject;
 class nsFrameLoader;
 class nsIBoxObject;
 
 // IID for the nsIDocument interface
 #define NS_IDOCUMENT_IID      \
-{ 0xe0ca6723, 0x1efa, 0x4819, \
-  { 0x84, 0xbb, 0xfa, 0x48, 0x39, 0xe8, 0xef, 0x19 } }
+  {0x1666cc78, 0x54ad, 0x4672, \
+      {0x93, 0x79, 0x9b, 0x6a, 0x61, 0x78, 0x94, 0x1a } }
 
 // Flag for AddStyleSheet().
 #define NS_STYLESHEET_FROM_CATALOG                (1 << 0)
 
 //----------------------------------------------------------------------
 
 // Document interface.  This is implemented by all document objects in
 // Gecko.
@@ -1160,16 +1160,34 @@ public:
    * Returns true if the locale used for the document specifies a direction of
    * right to left. For chrome documents, this comes from the chrome registry.
    * This is used to determine the current state for the :-moz-locale-dir pseudoclass
    * so once can know whether a document is expected to be rendered left-to-right
    * or right-to-left.
    */
   virtual PRBool IsDocumentRightToLeft() { return PR_FALSE; }
 
+  /**
+   * Gets the document's cached pointer to the first <base> element in this
+   * document which has an href attribute.  If the document doesn't contain any
+   * <base> elements with an href, returns null.
+   */
+  virtual nsIContent* GetFirstBaseNodeWithHref() = 0;
+
+  /**
+   * Sets the document's cached pointer to the first <base> element with an
+   * href attribute in this document and updates the document's base URI
+   * according to the element's href.
+   *
+   * If the given node is the same as the current first base node, this
+   * function still updates the document's base URI according to the node's
+   * href, if it changed.
+   */
+  virtual nsresult SetFirstBaseNodeWithHref(nsIContent *node) = 0;
+
 protected:
   ~nsIDocument()
   {
     // XXX The cleanup of mNodeInfoManager (calling DropDocumentReference and
     //     releasing it) happens in the nsDocument destructor. We'd prefer to
     //     do it here but nsNodeInfoManager is a concrete class that we don't
     //     want to expose to users of the nsIDocument API outside of Gecko.
   }
--- a/content/base/src/nsDocument.cpp
+++ b/content/base/src/nsDocument.cpp
@@ -1763,16 +1763,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
   }
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mChannel)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mStyleAttrStyleSheet)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mScriptEventManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mXPathEvaluatorTearoff)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mLayoutHistoryState)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mOnloadBlocker)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mFirstBaseNodeWithHref)
 
   // An element will only be in the linkmap as long as it's in the
   // document, so we'll traverse the table here instead of from the element.
   if (tmp->mLinkMap.IsInitialized()) {
     tmp->mLinkMap.EnumerateEntries(LinkMapTraverser, &cb);
   }
 
   // Traverse all our nsCOMArrays.
@@ -1814,16 +1815,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
   for (PRInt32 indx = PRInt32(tmp->mChildren.ChildCount()) - 1; 
        indx >= 0; --indx) {
     tmp->mChildren.ChildAt(indx)->UnbindFromTree();
     tmp->mChildren.RemoveChildAt(indx);
   }
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mCachedRootContent)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mDisplayDocument)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mFirstBaseNodeWithHref)
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK_USERDATA
 
   tmp->mParentDocument = nsnull;
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMARRAY(mPreloadingImages)
 
   // nsDocument has a pretty complex destructor, so we're going to
@@ -1966,22 +1968,19 @@ nsDocument::ResetToURI(nsIURI *aURI, nsI
   DestroyLinkMap();
 
   PRUint32 count = mChildren.ChildCount();
   { // Scope for update
     MOZ_AUTO_DOC_UPDATE(this, UPDATE_CONTENT_MODEL, PR_TRUE);    
     for (PRInt32 i = PRInt32(count) - 1; i >= 0; i--) {
       nsCOMPtr<nsIContent> content = mChildren.ChildAt(i);
 
-      // XXXbz this is backwards from how ContentRemoved normally works.  That
-      // is, usually it's dispatched after the content has been removed from
-      // the tree.
+      mChildren.RemoveChildAt(i);
       nsNodeUtils::ContentRemoved(this, content, i);
       content->UnbindFromTree();
-      mChildren.RemoveChildAt(i);
     }
   }
   mCachedRootContent = nsnull;
 
   // Reset our stylesheets
   ResetStylesheetsToURI(aURI);
   
   // Release the listener manager
@@ -1989,17 +1988,19 @@ nsDocument::ResetToURI(nsIURI *aURI, nsI
     mListenerManager->Disconnect();
     mListenerManager = nsnull;
   }
 
   // Release the stylesheets list.
   mDOMStyleSheets = nsnull;
 
   SetDocumentURI(aURI);
-  mDocumentBaseURI = mDocumentURI;
+  // If mDocumentBaseURI is null, nsIDocument::GetBaseURI() returns
+  // mDocumentURI.
+  mDocumentBaseURI = nsnull;
 
   if (aLoadGroup) {
     mDocumentLoadGroup = do_GetWeakReference(aLoadGroup);
     // there was an assertion here that aLoadGroup was not null.  This
     // is no longer valid: nsDocShell::SetDocument does not create a
     // load group, and it works just fine
 
     // XXXbz what does "just fine" mean exactly?  And given that there
@@ -2244,17 +2245,33 @@ nsDocument::StopDocumentLoad()
   if (mParser) {
     mParser->Terminate();
   }
 }
 
 void
 nsDocument::SetDocumentURI(nsIURI* aURI)
 {
+  nsCOMPtr<nsIURI> oldBase = nsIDocument::GetBaseURI();
   mDocumentURI = NS_TryToMakeImmutable(aURI);
+  nsIURI* newBase = nsIDocument::GetBaseURI();
+
+  PRBool equalBases = PR_FALSE;
+  if (oldBase && newBase) {
+    oldBase->Equals(newBase, &equalBases);
+  }
+  else {
+    equalBases = !oldBase && !newBase;
+  }
+
+  // If changing the document's URI changed the base URI of the document, we
+  // need to refresh the hrefs of all the links on the page.
+  if (!equalBases) {
+    RefreshLinkHrefs();
+  }
 }
 
 NS_IMETHODIMP
 nsDocument::GetLastModified(nsAString& aLastModified)
 {
   if (!mLastModified.IsEmpty()) {
     aLastModified.Assign(mLastModified);
   } else {
@@ -2784,27 +2801,43 @@ nsDocument::ReleaseCapture()
   return NS_OK;
 }
 
 nsresult
 nsDocument::SetBaseURI(nsIURI* aURI)
 {
   nsresult rv = NS_OK;
 
+  nsCOMPtr<nsIURI> oldBase = nsIDocument::GetBaseURI();
   if (aURI) {
     rv = nsContentUtils::GetSecurityManager()->
       CheckLoadURIWithPrincipal(NodePrincipal(), aURI,
                                 nsIScriptSecurityManager::STANDARD);
     if (NS_SUCCEEDED(rv)) {
       mDocumentBaseURI = NS_TryToMakeImmutable(aURI);
     }
   } else {
     mDocumentBaseURI = nsnull;
   }
 
+  nsIURI* newBase = nsIDocument::GetBaseURI();
+  PRBool equalBases = PR_FALSE;
+  if (oldBase && newBase) {
+    oldBase->Equals(newBase, &equalBases);
+  }
+  else {
+    equalBases = !oldBase && !newBase;
+  }
+
+  // If the document's base URI has changed, we need to re-resolve all the
+  // cached link hrefs relative to the new base.
+  if (!equalBases) {
+    RefreshLinkHrefs();
+  }
+
   return rv;
 }
 
 void
 nsDocument::GetBaseTarget(nsAString &aBaseTarget) const
 {
   aBaseTarget.Assign(mBaseTarget);
 }
@@ -5573,18 +5606,18 @@ nsDocument::IsSupported(const nsAString&
   return nsGenericElement::InternalIsSupported(static_cast<nsIDOMDocument*>(this),
                                                aFeature, aVersion, aReturn);
 }
 
 NS_IMETHODIMP
 nsDocument::GetBaseURI(nsAString &aURI)
 {
   nsCAutoString spec;
-  if (mDocumentBaseURI) {
-    mDocumentBaseURI->GetSpec(spec);
+  if (nsIDocument::GetBaseURI()) {
+    nsIDocument::GetBaseURI()->GetSpec(spec);
   }
 
   CopyUTF8toUTF16(spec, aURI);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
@@ -7337,37 +7370,41 @@ public:
     nsCAutoString spec;
     uri->GetSpec(spec);
     // We use nsCString::Equals here instead of nsIURI::Equals because
     // history matching is all based on spec equality
     if (!spec.Equals(matchURISpec))
       return;
 
     // Throw away the cached link state so it gets refetched by the style
-    // system      
+    // system.  We can't call ContentStatesChanged here, because that might
+    // modify the hashtable.  Instead, we'll just insert into this array and
+    // leave it to our caller to call ContentStatesChanged.
     aContent->SetLinkState(eLinkState_Unknown);
     contentVisited.AppendObject(aContent);
   }
 };
 
 void
 nsDocument::NotifyURIVisitednessChanged(nsIURI* aURI)
 {
   if (!mVisible) {
     mVisitednessChangedURIs.AppendObject(aURI);
     return;
   }
 
   nsUint32ToContentHashEntry* entry = mLinkMap.GetEntry(GetURIHash(aURI));
   if (!entry)
     return;
-  
+
   URIVisitNotifier visitor;
   aURI->GetSpec(visitor.matchURISpec);
   entry->VisitContent(&visitor);
+
+  MOZ_AUTO_DOC_UPDATE(this, UPDATE_CONTENT_STATE, PR_TRUE);
   for (PRUint32 count = visitor.contentVisited.Count(), i = 0; i < count; ++i) {
     ContentStatesChanged(visitor.contentVisited[i],
                          nsnull, NS_EVENT_STATE_VISITED);
   }
 }
 
 void
 nsDocument::DestroyLinkMap()
@@ -7378,24 +7415,131 @@ nsDocument::DestroyLinkMap()
 
 void
 nsDocument::UpdateLinkMap()
 {
   NS_ASSERTION(mVisible,
                "Should only be updating the link map in visible documents");
   if (!mVisible)
     return;
-    
+
   PRInt32 count = mVisitednessChangedURIs.Count();
   for (PRInt32 i = 0; i < count; ++i) {
     NotifyURIVisitednessChanged(mVisitednessChangedURIs[i]);
   }
   mVisitednessChangedURIs.Clear();
 }
 
+class RefreshLinkStateVisitor : public nsUint32ToContentHashEntry::Visitor
+{
+public:
+  nsCOMArray<nsIContent> contentVisited;
+
+  virtual void Visit(nsIContent* aContent) {
+    // We can't call ContentStatesChanged here, because that may modify the link
+    // map.  Instead, we just add to an array and call ContentStatesChanged
+    // later.
+    aContent->SetLinkState(eLinkState_Unknown);
+    contentVisited.AppendObject(aContent);
+  }
+};
+
+static PLDHashOperator
+RefreshLinkStateTraverser(nsUint32ToContentHashEntry* aEntry,
+                               void* userArg)
+{
+  RefreshLinkStateVisitor *visitor =
+    static_cast<RefreshLinkStateVisitor*>(userArg);
+
+  aEntry->VisitContent(visitor);
+  return PL_DHASH_NEXT;
+}
+
+
+// Helper function for nsDocument::RefreshLinkHrefs
+static void
+DropCachedHrefsRecursive(nsIContent * const elem)
+{
+  // Drop the element's cached href, if it has one.  (If it doesn't have
+  // one, this call does nothing.)  We could check first that elem is an <a>
+  // tag to avoid making a virtual call, but it turns out not to make a
+  // substantial perf difference either way.  This doesn't restyle the link,
+  // but we do that later.
+  elem->DropCachedHref();
+
+  PRUint32 childCount;
+  nsIContent * const * child = elem->GetChildArray(&childCount);
+  nsIContent * const * end = child + childCount;
+  for ( ; child != end; ++child) {
+    DropCachedHrefsRecursive(*child);
+  }
+}
+
+void
+nsDocument::RefreshLinkHrefs()
+{
+  if (!GetRootContent())
+    return;
+
+  // First, walk the DOM and clear the cached hrefs of all the <a> tags.
+  DropCachedHrefsRecursive(GetRootContent());
+
+  // Now update the styles of everything in the linkmap.
+  RefreshLinkStateVisitor visitor;
+  mLinkMap.EnumerateEntries(RefreshLinkStateTraverser, &visitor);
+
+  MOZ_AUTO_DOC_UPDATE(this, UPDATE_CONTENT_STATE, PR_TRUE);
+  for (PRUint32 count = visitor.contentVisited.Count(), i = 0; i < count; i++) {
+    ContentStatesChanged(visitor.contentVisited[i],
+                         nsnull, NS_EVENT_STATE_VISITED);
+  }
+}
+
+nsIContent*
+nsDocument::GetFirstBaseNodeWithHref()
+{
+  return mFirstBaseNodeWithHref;
+}
+
+nsresult
+nsDocument::SetFirstBaseNodeWithHref(nsIContent *elem)
+{
+  mFirstBaseNodeWithHref = elem;
+
+  if (!elem) {
+    SetBaseURI(nsnull);
+    return NS_OK;
+  }
+
+  NS_ASSERTION(elem->Tag() == nsGkAtoms::base,
+               "Setting base node to a non <base> element?");
+  NS_ASSERTION(elem->GetNameSpaceID() == kNameSpaceID_XHTML,
+               "Setting base node to a non XHTML element?");
+
+  nsIDocument* doc = elem->GetOwnerDoc();
+  nsIURI* currentURI = nsIDocument::GetDocumentURI();
+
+  // Resolve the <base> element's href relative to our current URI
+  nsAutoString href;
+  PRBool hasHref = elem->GetAttr(kNameSpaceID_None, nsGkAtoms::href, href);
+  NS_ASSERTION(hasHref,
+               "Setting first base node to a node with no href attr?");
+
+  nsCOMPtr<nsIURI> newBaseURI;
+  nsContentUtils::NewURIWithDocumentCharset(
+    getter_AddRefs(newBaseURI), href, doc, currentURI);
+
+  // Try to set our base URI.  If that fails, try to set our base URI to null.
+  nsresult rv =  SetBaseURI(newBaseURI);
+  if (NS_FAILED(rv)) {
+    return SetBaseURI(nsnull);
+  }
+  return rv;
+}
+
 NS_IMETHODIMP
 nsDocument::GetScriptTypeID(PRUint32 *aScriptType)
 {
     NS_ERROR("No default script type here - ask some element");
     return nsIProgrammingLanguage::UNKNOWN;
 }
 
 NS_IMETHODIMP
--- a/content/base/src/nsDocument.h
+++ b/content/base/src/nsDocument.h
@@ -1028,22 +1028,28 @@ protected:
 
   void DispatchContentLoadedEvents();
 
   void RetrieveRelevantHeaders(nsIChannel *aChannel);
 
   static PRBool TryChannelCharset(nsIChannel *aChannel,
                                   PRInt32& aCharsetSource,
                                   nsACString& aCharset);
-  
+
   void UpdateLinkMap();
   // Call this before the document does something that will unbind all content.
   // That will stop us from resolving URIs for all links as they are removed.
   void DestroyLinkMap();
 
+  // Refreshes the hrefs of all the links in the document.
+  void RefreshLinkHrefs();
+
+  nsIContent* GetFirstBaseNodeWithHref();
+  nsresult SetFirstBaseNodeWithHref(nsIContent *node);
+
   // Get the root <html> element, or return null if there isn't one (e.g.
   // if the root isn't <html>)
   nsIContent* GetHtmlContent();
   // Returns the first child of GetHtmlContent which has the given tag,
   // or nsnull if that doesn't exist.
   nsIContent* GetHtmlChildContent(nsIAtom* aTag);
   // Get the canonical <body> element, or return null if there isn't one (e.g.
   // if the root isn't <html> or if the <body> isn't there)
@@ -1208,16 +1214,18 @@ protected:
 
   // Our update nesting level
   PRUint32 mUpdateNestLevel;
 
   // The application cache that this document is associated with, if
   // any.  This can change during the lifetime of the document.
   nsCOMPtr<nsIApplicationCache> mApplicationCache;
 
+  nsCOMPtr<nsIContent> mFirstBaseNodeWithHref;
+
 private:
   friend class nsUnblockOnloadEvent;
 
   void PostUnblockOnloadEvent();
   void DoUnblockOnload();
 
   /**
    * See if aDocument is a child of this.  If so, return the frame element in
--- a/content/html/content/src/nsGenericHTMLElement.cpp
+++ b/content/html/content/src/nsGenericHTMLElement.cpp
@@ -2155,17 +2155,21 @@ nsGenericHTMLElement::GetURIAttr(nsIAtom
   // Don't care about return value.  If it fails, we still want to
   // return PR_TRUE, and *aURI will be null.
   nsContentUtils::NewURIWithDocumentCharset(aURI,
                                             isURIAttr ?
                                               attr->GetURIStringValue() :
                                               attr->GetStringValue(),
                                             GetOwnerDoc(), baseURI);
 
-  if (isURIAttr) {
+  // We may have to re-resolve all our cached hrefs when the document's base
+  // URI changes.  The base URI depends on the owner document, but it's the
+  // current document that keeps track of links.  If the two documents don't
+  // match, we shouldn't cache.
+  if (isURIAttr && GetOwnerDoc() == GetCurrentDoc()) {
     const_cast<nsAttrValue*>(attr)->CacheURIValue(*aURI);
   }
   return PR_TRUE;
 }
 
 nsresult
 nsGenericHTMLElement::GetURIListAttr(nsIAtom* aAttr, nsAString& aResult)
 {
--- a/content/html/content/src/nsHTMLAnchorElement.cpp
+++ b/content/html/content/src/nsHTMLAnchorElement.cpp
@@ -122,16 +122,18 @@ public:
                              PRBool aNotify);
   virtual PRBool ParseAttribute(PRInt32 aNamespaceID,
                                 nsIAtom* aAttribute,
                                 const nsAString& aValue,
                                 nsAttrValue& aResult);
 
   virtual nsresult Clone(nsINodeInfo *aNodeInfo, nsINode **aResult) const;
 
+  virtual void DropCachedHref();
+
 protected:
   void ResetLinkCacheState();
   
   // The cached visited state
   nsLinkState mLinkState;
 };
 
 
@@ -482,16 +484,28 @@ nsHTMLAnchorElement::ParseAttribute(PRIn
     return aResult.ParseLazyURIValue(aValue);
   }
 
   return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
                                               aResult);
 }
 
 void
+nsHTMLAnchorElement::DropCachedHref()
+{
+  nsAttrValue* attr =
+    const_cast<nsAttrValue*>(mAttrsAndChildren.GetAttr(nsGkAtoms::href));
+
+  if (!attr || attr->Type() != nsAttrValue::eLazyURIValue)
+    return;
+
+  attr->DropCachedURI();
+}
+
+void
 nsHTMLAnchorElement::ResetLinkCacheState()
 {
   nsIDocument* doc = GetCurrentDoc();
   if (doc) {
     doc->ForgetLink(this);
   }
   mLinkState = eLinkState_Unknown;
 
--- a/content/html/content/src/nsHTMLSharedElement.cpp
+++ b/content/html/content/src/nsHTMLSharedElement.cpp
@@ -42,16 +42,17 @@
 #include "nsIDOMHTMLQuoteElement.h"
 #include "nsIDOMHTMLBaseFontElement.h"
 #include "nsGenericHTMLElement.h"
 #include "nsGkAtoms.h"
 #include "nsStyleConsts.h"
 #include "nsPresContext.h"
 #include "nsRuleData.h"
 #include "nsMappedAttributes.h"
+#include "nsNetUtil.h"
 
 // XXX nav4 has type= start= (same as OL/UL)
 extern nsAttrValue::EnumTable kListTypeTable[];
 
 class nsHTMLSharedElement : public nsGenericHTMLElement,
                             public nsIDOMHTMLIsIndexElement,
                             public nsIDOMHTMLParamElement,
                             public nsIDOMHTMLBaseElement,
@@ -97,23 +98,41 @@ public:
   // nsIDOMHTMLBaseFontElement
   NS_DECL_NSIDOMHTMLBASEFONTELEMENT
 
   // nsIContent
   virtual PRBool ParseAttribute(PRInt32 aNamespaceID,
                                 nsIAtom* aAttribute,
                                 const nsAString& aValue,
                                 nsAttrValue& aResult);
+  nsresult SetAttr(PRInt32 aNameSpaceID, nsIAtom* aName,
+                   const nsAString& aValue, PRBool aNotify)
+  {
+    return SetAttr(aNameSpaceID, aName, nsnull, aValue, aNotify);
+  }
+  virtual nsresult SetAttr(PRInt32 aNameSpaceID, nsIAtom* aName,
+                           nsIAtom* aPrefix, const nsAString& aValue,
+                           PRBool aNotify);
+
+  virtual nsresult UnsetAttr(PRInt32 aNameSpaceID, nsIAtom* aName,
+                             PRBool aNotify);
+
+  virtual nsresult BindToTree(nsIDocument* aDocument, nsIContent* aParent,
+                              nsIContent* aBindingParent,
+                              PRBool aCompileEventHandlers);
+
+  virtual void UnbindFromTree(PRBool aDeep = PR_TRUE,
+                              PRBool aNullParent = PR_TRUE);
+
   virtual nsMapRuleToAttributesFunc GetAttributeMappingFunction() const;
   NS_IMETHOD_(PRBool) IsAttributeMapped(const nsIAtom* aAttribute) const;
 
   virtual nsresult Clone(nsINodeInfo *aNodeInfo, nsINode **aResult) const;
 };
 
-
 NS_IMPL_NS_NEW_HTML_ELEMENT(Shared)
 
 
 nsHTMLSharedElement::nsHTMLSharedElement(nsINodeInfo *aNodeInfo)
   : nsGenericHTMLElement(aNodeInfo)
 {
 }
 
@@ -380,16 +399,166 @@ nsHTMLSharedElement::IsAttributeMapped(c
     };
 
     return FindAttributeDependence(aAttribute, map, NS_ARRAY_LENGTH(map));
   }
 
   return nsGenericHTMLElement::IsAttributeMapped(aAttribute);
 }
 
+nsresult
+nsHTMLSharedElement::SetAttr(PRInt32 aNameSpaceID, nsIAtom* aName,
+                             nsIAtom* aPrefix, const nsAString& aValue,
+                             PRBool aNotify)
+{
+  nsresult rv =  nsGenericHTMLElement::SetAttr(aNameSpaceID, aName, aPrefix,
+                                               aValue, aNotify);
+
+  // If the href attribute of a <base> tag is changing, we may need to update
+  // the document's base URI, which will cause all the links on the page to be
+  // re-resolved given the new base.
+  if (NS_SUCCEEDED(rv) &&
+      mNodeInfo->Equals(nsGkAtoms::base, kNameSpaceID_XHTML) &&
+      aName == nsGkAtoms::href &&
+      aNameSpaceID == kNameSpaceID_None &&
+      GetOwnerDoc() == GetCurrentDoc()) {
+
+    nsIDocument* doc = GetCurrentDoc();
+    NS_ENSURE_TRUE(doc, NS_OK);
+
+    // We become the first base node with an href if
+    //   * there's no other base node with an href, or
+    //   * we come before the first base node with an href (this would happen
+    //     if we didn't have an href before this call to SetAttr).
+    // Additionally, we call doc->SetFirstBaseNodeWithHref if we're the first
+    // base node with an href so the document updates its base URI with our new
+    // href.
+    nsIContent* firstBase = doc->GetFirstBaseNodeWithHref();
+    if (!firstBase || this == firstBase ||
+        nsContentUtils::PositionIsBefore(this, firstBase)) {
+
+      return doc->SetFirstBaseNodeWithHref(this);
+    }
+  }
+
+  return rv;
+}
+
+// Helper function for nsHTMLSharedElement::UnbindFromTree.  Finds and returns
+// the first <base> tag with an href attribute which is a child of elem, if one
+// exists.
+static nsIContent*
+FindBaseRecursive(nsINode * const elem)
+{
+  // We can't use NS_GetContentList to get the list of <base> elements, because
+  // that flushes content notifications, and we need this function to work in
+  // UnbindFromTree.  Once we land the HTML5 parser and get rid of content
+  // notifications, we should fix this up. (bug 515819)
+
+  PRUint32 childCount;
+  nsIContent * const * child = elem->GetChildArray(&childCount);
+  nsIContent * const * end = child + childCount;
+  for ( ; child != end; child++) {
+    nsIContent *childElem = *child;
+
+    if (childElem->NodeInfo()->Equals(nsGkAtoms::base, kNameSpaceID_XHTML) &&
+        childElem->HasAttr(kNameSpaceID_None, nsGkAtoms::href))
+      return childElem;
+
+    nsIContent* base = FindBaseRecursive(childElem);
+    if (base)
+      return base;
+  }
+
+  return nsnull;
+}
+
+nsresult
+nsHTMLSharedElement::UnsetAttr(PRInt32 aNameSpaceID, nsIAtom* aName,
+                               PRBool aNotify)
+{
+  nsresult rv = nsGenericHTMLElement::UnsetAttr(aNameSpaceID, aName, aNotify);
+
+  // If we're the first <base> with an href and our href attribute is being
+  // unset, then we're no longer the first <base> with an href, and we need to
+  // find the new one.
+  if (NS_SUCCEEDED(rv) &&
+      mNodeInfo->Equals(nsGkAtoms::base, kNameSpaceID_XHTML) &&
+      aName == nsGkAtoms::href &&
+      aNameSpaceID == kNameSpaceID_None &&
+      GetOwnerDoc() == GetCurrentDoc()) {
+
+    nsIDocument* doc = GetCurrentDoc();
+    NS_ENSURE_TRUE(doc, NS_OK);
+
+    // If we're not the first <base> in the document, then unsetting our href
+    // doesn't affect the document's base URI.
+    if (this != doc->GetFirstBaseNodeWithHref())
+      return NS_OK;
+
+    // We're the first base, but we don't have an href; find the first base
+    // which does have an href, and set the document's first base to that.
+    nsIContent* newBaseNode = FindBaseRecursive(doc);
+    return doc->SetFirstBaseNodeWithHref(newBaseNode);
+  }
+
+  return rv;
+}
+
+nsresult
+nsHTMLSharedElement::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
+                                nsIContent* aBindingParent,
+                                PRBool aCompileEventHandlers)
+{
+  nsresult rv = nsGenericHTMLElement::BindToTree(aDocument, aParent,
+                                                 aBindingParent,
+                                                 aCompileEventHandlers);
+
+  // The document stores a pointer to its first <base> element, which we may
+  // need to update here.
+  if (NS_SUCCEEDED(rv) &&
+      mNodeInfo->Equals(nsGkAtoms::base, kNameSpaceID_XHTML) &&
+      HasAttr(kNameSpaceID_None, nsGkAtoms::href) &&
+      aDocument) {
+
+    // If there's no <base> in the document, or if this comes before the one
+    // that's currently there, set the document's first <base> to this.
+    nsINode* curBaseNode = aDocument->GetFirstBaseNodeWithHref();
+    if (!curBaseNode ||
+        nsContentUtils::PositionIsBefore(this, curBaseNode)) {
+
+      aDocument->SetFirstBaseNodeWithHref(this);
+    }
+  }
+
+  return rv;
+}
+
+void
+nsHTMLSharedElement::UnbindFromTree(PRBool aDeep, PRBool aNullParent)
+{
+  nsCOMPtr<nsIDocument> doc = GetCurrentDoc();
+
+  nsGenericHTMLElement::UnbindFromTree(aDeep, aNullParent);
+
+  // If we're removing a <base> from a document, we may need to update the
+  // document's record of the first base node.
+  if (doc && mNodeInfo->Equals(nsGkAtoms::base, kNameSpaceID_XHTML)) {
+
+    // If we're not the first base node, then we don't need to do anything.
+    if (this != doc->GetFirstBaseNodeWithHref())
+      return;
+
+    // If we were the first base node, we need to find the new first base.
+
+    nsIContent* newBaseNode = FindBaseRecursive(doc);
+    doc->SetFirstBaseNodeWithHref(newBaseNode);
+  }
+}
+
 nsMapRuleToAttributesFunc
 nsHTMLSharedElement::GetAttributeMappingFunction() const
 {
   if (mNodeInfo->Equals(nsGkAtoms::spacer)) {
     return &SpacerMapAttributesIntoRule;
   }
   else if (mNodeInfo->Equals(nsGkAtoms::dir) ||
            mNodeInfo->Equals(nsGkAtoms::menu)) {
--- a/content/html/content/test/Makefile.in
+++ b/content/html/content/test/Makefile.in
@@ -66,16 +66,20 @@ include $(topsrcdir)/config/rules.mk
 		test_bug242709.html \
 		bug277724_iframe1.html \
 		bug277724_iframe2.xhtml \
 		test_bug277724.html \
 		bug277890_iframe.html \
 		bug277890_load.html \
 		test_bug277890.html \
 		test_bug287465.html \
+		test_bug209275.xhtml \
+		file_bug209275_1.html \
+		file_bug209275_2.html \
+		file_bug209275_3.html \
 		test_bug295561.html \
 		test_bug300691-1.html \
 		test_bug300691-2.html \
 		test_bug300691-3.xhtml \
 		test_bug330705-1.html \
 		test_bug332246.html \
 		test_bug332893-1.html \
 		test_bug332893-2.html \
new file mode 100644
--- /dev/null
+++ b/content/html/content/test/file_bug209275_1.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <base href="http://example.org" />
+</head>
+<body onload="load();">
+Initial state
+
+<script>
+function load() {
+  // Nuke and rebuild the page.
+  document.removeChild(document.documentElement);
+  var html = document.createElement("html");
+  var body = document.createElement("body");
+  html.appendChild(body);
+  var link = document.createElement("a");
+  link.href = "#";
+  link.id = "link";
+  body.appendChild(link);
+  document.appendChild(html);
+
+  // Tell our parent to have a look at us.
+  parent.gGen.next();
+}
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/content/html/content/test/file_bug209275_2.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <base href="http://example.com" />
+</head>
+<body onload="load();">
+Page 2 initial state
+
+<script>
+function load() {
+  // Nuke and rebuild the page.
+  document.removeChild(document.documentElement);
+  html = document.createElement("html");
+  html.innerHTML = "<body><a href='/' id='link'>B</a></body>"
+  document.appendChild(html);
+
+  // Tell our parent to have a look at us
+  parent.gGen.next();
+}
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/content/html/content/test/file_bug209275_3.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <base href="http://example.org" />
+</head>
+<body onload="load();">
+Initial state
+
+<script>
+function load() {
+  // Nuke and rebuild the page.  If document.open() clears the <base> properly,
+  // our new <base> will take precedence and the test will pass.
+  document.open();
+  document.write("<html><body><a id='link' href='/'>A</a>" +
+                 "<base href='http://localhost:8888' /></body></html>");
+
+  // Tell our parent to have a look at us.
+  parent.gGen.next();
+}
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/content/html/content/test/test_bug209275.xhtml
@@ -0,0 +1,261 @@
+<!DOCTYPE html [
+<!ATTLIST foo:base
+   id     ID       #IMPLIED
+>
+]>
+<html xmlns:foo="http://foo.com" xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=209275
+-->
+<head>
+  <title>Test for Bug 209275</title>
+  <script type="text/javascript" src="/MochiKit/packed.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+  <style>
+    @namespace svg url("http://www.w3.org/2000/svg");
+    svg|a           { fill:blue; }
+    svg|a:visited   { fill:purple; }
+  </style>
+
+  <!--
+    base0 should be ignored because it's not in the XHTML namespace
+  -->
+  <foo:base id="base0" href="http://www.foo.com" />
+
+  <!--
+    baseEmpty should be ignored because it has no href and never gets one.
+  -->
+  <base id="baseEmpty" />
+
+  <!--
+    baseWrongAttrNS should be ignored because its href attribute isn't in the empty
+    namespace.
+  -->
+  <base id="baseWrongAttrNS" foo:href="http://foo.com" />
+
+  <base id="base1" />
+  <base id="base2" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=209275">Mozilla Bug 209275</a>
+<p id="display">
+</p>
+<div id="content">
+  <a href="/" id="link1">link1</a>
+  <div style="display:none">
+    <a href="/" id="link2">link2</a>
+  </div>
+  <a href="/" id="link3" style="display:none">link3</a>
+  <a href="#" id="link4">link4</a>
+  <a href=""  id="colorlink">colorlink</a>
+  <a href="#" id="link5">link5</a>
+  <iframe id="iframe"></iframe>
+
+  <svg width="5cm" height="3cm" viewBox="0 0 5 3" version="1.1"
+     xmlns="http://www.w3.org/2000/svg"
+     xmlns:xlink="http://www.w3.org/1999/xlink">
+  <a xlink:href="http://www.w3.org" id="ellipselink">
+    <ellipse cx="2.5" cy="1.5" rx="2" ry="1" id="ellipse" />
+  </a>
+  </svg>
+
+</div>
+<pre id="test">
+<script type="text/javascript;version=1.7">
+<![CDATA[
+
+/** Test for Bug 209275 **/
+SimpleTest.waitForExplicitFinish();
+
+function link123HrefIs(href, testNum) {
+  is($('link1').href, href, "link1 test " + testNum);
+  is($('link2').href, href, "link2 test " + testNum);
+  is($('link3').href, href, "link3 test " + testNum);
+}
+
+var gGen;
+
+function getColor(elem) {
+  return document.defaultView.getComputedStyle(elem, "").color;
+}
+
+function getFill(elem) {
+  return document.defaultView
+          .getComputedStyle(elem, "")
+          .getPropertyValue("fill");
+}
+
+function setXlinkHref(elem, href) {
+  elem.setAttributeNS("http://www.w3.org/1999/xlink", "href", href);
+}
+
+var iframe = document.getElementById("iframe");
+var iframeCw = iframe.contentWindow;
+
+function run() {
+  var iframe = document.getElementById("iframe");
+  var iframeCw = iframe.contentWindow;
+
+  // First, get the visited/unvisited link/ellipse colors.
+  var unvisitedColor;
+  var visitedColor;
+  var unvisitedFill;
+  var visitedFill;
+
+  var rand = Date.now() + "-" + Math.random();
+  // Set colorlink and ellipselink's hrefs
+  $("colorlink").href = rand;
+  setXlinkHref($("ellipselink"), rand);
+  unvisitedColor = getColor($("colorlink"));
+  unvisitedFill = getFill($("ellipse"));
+
+  // Set the link's href to our current location so we can get the visited link
+  // color.
+  $("colorlink").href = document.location;
+  setXlinkHref($("ellipselink"), document.location);
+  visitedColor = getColor($("colorlink"));
+  visitedFill = getFill($("ellipse"));
+  isnot(visitedColor, unvisitedColor,
+        "visited/unvisited link colors are the same?");
+  isnot(visitedFill, unvisitedFill,
+        "visited/unvisited ellipse fill colors are the same?");
+
+  // Now we can start the tests in earnest.
+
+  var loc = location;
+  // everything from the location up to and including the final forward slash
+  var path = /(.*\/)[^\/]*/.exec(location)[1];
+
+  // Set colorlink's href so we can check that it changes colors after we
+  // change the base href.
+  $('colorlink').href = "http://example.com/" + rand;
+  setXlinkHref($("ellipse"), "http://example.com/" + rand);
+
+  // Load http://example.com/${rand} into an iframe so we can test that changing
+  // the document's base changes the visitedness of our links.
+  iframe.onload = function() { gGen.next(); };
+  iframeCw.location = "http://example.com/" + rand;
+  yield; // wait for onload to fire.
+
+  // Make sure things are what as we expect them at the beginning.
+  link123HrefIs("http://localhost:8888/", 1);
+  is($('link4').href, loc + "#", "link 4 test 1");
+  is($('link5').href, loc + "#", "link 5 test 1");
+
+  // Remove link5 from the document.  We're going to test that its href changes
+  // properly when we change our base.
+  var link5 = $('link5');
+  link5.parentNode.removeChild(link5);
+
+  $('base1').href = "http://example.com";
+
+  // Were the links' hrefs updated after the base change?
+  link123HrefIs("http://example.com/", 2);
+  is($('link4').href, "http://example.com/#", "link 4 test 2");
+  is(link5.href, "http://example.com/#", "link 5 test 2");
+
+  // Were colorlink's color and ellipse's fill updated appropriately?
+  is(getColor($('colorlink')), visitedColor,
+     "Wrong link color after base change.");
+  is(getFill($('ellipse')), visitedFill,
+     "Wrong ellipse fill after base change.");
+
+  $('base1').href = "foo/";
+  // Should be interpreted relative to current URI (not the current base), so
+  // base should now be http://localhost:8888/foo/
+
+  link123HrefIs("http://localhost:8888/", 3);
+  is($('link4').href, path + "foo/#", "link 4 test 3");
+
+  // Changing base2 shouldn't affect anything, because it's not the first base
+  // tag.
+  $('base2').href = "http://example.org/bar/";
+  link123HrefIs("http://localhost:8888/", 4);
+  is($('link4').href, path + "foo/#", "link 4 test 4");
+
+  // If we unset base1's href attribute, the document's base should come from
+  // base2, whose href is http://example.org/bar/.
+  $('base1').removeAttribute("href");
+  link123HrefIs("http://example.org/", 5);
+  is($('link4').href, "http://example.org/bar/#", "link 4 test 5");
+
+  // If we remove base1, base2 should become the first base tag, and the hrefs
+  // of all the links should change accordingly.
+  $('base1').parentNode.removeChild($('base1'));
+  link123HrefIs("http://example.org/", 6);
+  is($('link4').href, "http://example.org/bar/#", "link 4 test 6");
+
+  // If we add a new base after base2, nothing should change.
+  var base3 = document.createElement("base");
+  base3.href = "http://base3.example.org/";
+  $('base2').parentNode.insertBefore(base3, $('base2').nextSibling);
+  link123HrefIs("http://example.org/", 7);
+  is($('link4').href, "http://example.org/bar/#", "link 4 test 7");
+
+  // But now if we add a new base before base 2, it should become the primary
+  // base.
+  var base4 = document.createElement("base");
+  base4.href = "http://base4.example.org/";
+  $('base2').parentNode.insertBefore(base4, $('base2'));
+  link123HrefIs("http://base4.example.org/", 8);
+  is($('link4').href, "http://base4.example.org/#", "link 4 test 8");
+
+  // Now if we remove all the base tags, the base should become the page's URI
+  // again.
+  $('base2').parentNode.removeChild($('base2'));
+  base3.parentNode.removeChild(base3);
+  base4.parentNode.removeChild(base4);
+
+  link123HrefIs("http://localhost:8888/", 9);
+  is($('link4').href, loc + "#", "link 4 test 9");
+
+  // Setting the href of base0 shouldn't do anything because it's not in the
+  // XHTML namespace.
+  $('base0').href = "http://bar.com";
+  link123HrefIs("http://localhost:8888/", 10);
+  is($('link4').href, loc + "#", "link 4 test 10");
+
+  // We load into an iframe a document with a <base href="...">, then remove
+  // the document element.  Then we add an <html>, <body>, and <a>, and make
+  // sure that the <a> is resolved relative to the page's location, not its
+  // original base.  We do this twice, rebuilding the document in a different
+  // way each time.
+
+  iframe.onload = null;
+  iframeCw.location = "file_bug209275_1.html";
+  yield; // wait for our child to call us back.
+  is(iframeCw.document.getElementById("link").href,
+     path + "file_bug209275_1.html#",
+     "Wrong href after nuking document.");
+
+  iframeCw.location = "file_bug209275_2.html";
+  yield; // wait for callback from child
+  is(iframeCw.document.getElementById("link").href,
+     "http://localhost:8888/",
+     "Wrong href after nuking document second time.");
+
+  // Make sure that document.open() makes the document forget about any <base>
+  // tags it has.
+  iframeCw.location = "file_bug209275_3.html";
+  yield; // wait for callback from child
+  is(iframeCw.document.getElementById("link").href,
+     "http://localhost:8888/",
+     "Wrong href after document.open().");
+
+  SimpleTest.finish();
+  yield;
+}
+
+window.addEventListener("load", function() {
+  gGen = run();
+  gGen.next();
+}, false);
+
+]]>
+</script>
+</pre>
+</body>
+</html>
+