/* GNU LESSER GENERAL PUBLIC LICENSE Copyright (C) 2006 The Lobo Project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Contact info: lobochief@users.sourceforge.net */ package org.lobobrowser.html.domimpl; import java.net.MalformedURLException; import java.net.URL; import java.util.Optional; import org.eclipse.jdt.annotation.NonNull; import org.lobobrowser.html.HtmlRendererContext; import org.lobobrowser.html.js.Window; import org.lobobrowser.html.style.CSSUtilities; import org.lobobrowser.js.HideFromJS; import org.lobobrowser.ua.UserAgentContext; import org.lobobrowser.util.Urls; import org.w3c.dom.css.CSSStyleSheet; import org.w3c.dom.html.HTMLLinkElement; import org.w3c.dom.stylesheets.LinkStyle; import co.uproot.css.domimpl.JStyleSheetWrapper; import cz.vutbr.web.css.StyleSheet; public class HTMLLinkElementImpl extends HTMLAbstractUIElement implements HTMLLinkElement, LinkStyle { private JStyleSheetWrapper styleSheet; public HTMLLinkElementImpl(final String name) { super(name); } private boolean disabled; public boolean getDisabled() { return this.disabled; } public void setDisabled(final boolean disabled) { this.disabled = disabled; final CSSStyleSheet sheet = this.styleSheet; if (sheet != null) { sheet.setDisabled(disabled); } } //TODO hide from JS public void setDisabledImpl(final boolean disabled) { this.disabled = disabled; } public String getHref() { final String href = this.getAttribute("href"); return href == null ? "" : Urls.removeControlCharacters(href); } public void setHref(final String href) { this.setAttribute("href", href); } public String getHreflang() { return this.getAttribute("hreflang"); } public void setHreflang(final String hreflang) { this.setAttribute("hreflang", hreflang); } public String getMedia() { return this.getAttribute("media"); } public void setMedia(final String media) { this.setAttribute("media", media); } public String getRel() { return this.getAttribute("rel"); } public void setRel(final String rel) { this.setAttribute("rel", rel); } public String getRev() { return this.getAttribute("rev"); } public void setRev(final String rev) { this.setAttribute("rev", rev); } public String getTarget() { final String target = this.getAttribute("target"); if (target != null) { return target; } final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; return doc == null ? null : doc.getDefaultTarget(); } public void setTarget(final String target) { this.setAttribute("target", target); } public String getType() { return this.getAttribute("type"); } public void setType(final String type) { this.setAttribute("type", type); } // TODO can go in Urls util class. private boolean isWellFormedURL() { final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.getOwnerDocument(); try { final URL baseURL = new URL(doc.getBaseURI()); // we call createURL just to check whether it throws an exception // if the URL is not well formed. Urls.createURL(baseURL, this.getHref()); return true; } catch (final MalformedURLException mfe) { // this.warn("Will not parse CSS. URI=[" + this.getHref() + "] with BaseURI=[" + doc.getBaseURI() + "] does not appear to be a valid URI."); return false; } } private Optional<@NonNull URL> getAbsoluteURL() { final String href = this.getHref(); if (href.startsWith("javascript:")) { return Optional.empty(); } else { try { return Optional.ofNullable(this.getFullURL(href)); } catch (final MalformedURLException mfu) { this.warn("Malformed URI: [" + href + "].", mfu); } } return Optional.empty(); } @HideFromJS public String getAbsoluteHref() { // TODO: Use Either in getAbsoluteURL and use the branch type for javascript return getAbsoluteURL().map(u -> u.toExternalForm()).orElse(getHref()); } // TODO: Should HTMLLinkElement actually support navigation? The Link element seems to be conflated with <a> elements @HideFromJS public boolean navigate() { // If there is no href attribute, chromium only dispatches the handlers without starting a navigation final String hrefAttr = this.getAttribute("href"); if (hrefAttr == null) { return false; } if (this.disabled) { return false; } final String href = getHref(); if (href.startsWith("#")) { // TODO: Scroll to the element. Issue #101 } else if (href.startsWith("javascript:")) { final String script = href.substring(11); // evalInScope adds the JS task ((Window) (((HTMLDocumentImpl) document).getDefaultView())).evalInScope(script); } else { final Optional<@NonNull URL> urlOpt = getAbsoluteURL(); if (urlOpt.isPresent()) { final HtmlRendererContext rcontext = this.getHtmlRendererContext(); final String target = this.getTarget(); rcontext.linkClicked(this, urlOpt.get(), target); return true; } } return false; } /* * Not used anymore after removal of createRenderState. However, it can be re-implemented using * HTMLElementImple.elementMatchCondition. * Note that there are privacy implications here. It is better to understand them before * re-implementing. private java.awt.Color getLinkColor() { final HTMLDocument doc = (HTMLDocument) this.document; if (doc != null) { final HTMLBodyElement body = (HTMLBodyElement) doc.getBody(); if (body != null) { final String vlink = body.getVLink(); final String link = body.getLink(); if (vlink != null || link != null) { final HtmlRendererContext rcontext = this.getHtmlRendererContext(); if (rcontext != null) { final boolean visited = rcontext.isVisitedLink(this); final String colorText = visited ? vlink : link; if (colorText != null) { return ColorFactory.getInstance().getColor(colorText); } } } } } return java.awt.Color.BLUE; }*/ /* protected RenderState createRenderState(RenderState prevRenderState) { if (this.hasAttribute("href")) { // Removed the following three as part of #135 // prevRenderState = new TextDecorationRenderState(prevRenderState, RenderState.MASK_TEXTDECORATION_UNDERLINE); // prevRenderState = new ColorRenderState(prevRenderState, this.getLinkColor()); // prevRenderState = new CursorRenderState(prevRenderState, Optional.of(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))); } return super.createRenderState(prevRenderState); }*/ @Override public String toString() { // Javascript code often depends on this being exactly href. See js9.html. // To change, perhaps add method to AbstractScriptableDelegate. // Chromium 37 and FF 32 both return the full url // return this.getHref(); return getAbsoluteHref(); } /** * Sets the owner node to null so as to update the old reference of the * stylesheet held by JS */ private void detachStyleSheet() { if (this.styleSheet != null) { this.styleSheet.setOwnerNode(null); this.styleSheet = null; final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.getOwnerDocument(); doc.styleSheetManager.invalidateStyles(); } } private boolean isSameRel(final String name, final String oldValue) { if ("rel".equals(name)) { if (this.isSameAttributeValue("rel", oldValue)) { return true; } } return false; } private boolean isSameHref(final String name, final String oldValue) { if ("href".equals(name)) { if (this.isSameAttributeValue("href", oldValue)) { return true; } } return false; } private boolean isSameAttributeValue(final String name, final String oldValue) { final String newValue = this.getAttribute(name); if (oldValue == null) { return newValue == null; } else { return oldValue.equals(newValue); } } private String getCleanRel() { final String rel = this.getRel(); return rel == null ? null : rel.trim().toLowerCase(); } private boolean isStyleSheet() { final String rel = this.getCleanRel(); return ((rel != null) && (rel.equals("stylesheet"))); } private boolean isAltStyleSheet() { final String rel = this.getCleanRel(); return ((rel != null) && (rel.equals("alternate stylesheet"))); } private boolean isAllowedRel() { return ((isStyleSheet()) || (isAltStyleSheet())); } private boolean isAllowedType() { final String type = this.getType(); return ((type == null) || (type.trim().length() == 0) || (type.equalsIgnoreCase("text/css"))); } private void processLink() { final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.getOwnerDocument(); try { final UserAgentContext uacontext = this.getUserAgentContext(); if (uacontext.isExternalCSSEnabled()) { try { final String href = this.getHref(); final StyleSheet jSheet = CSSUtilities.jParse(this, href, doc, doc.getBaseURI(), false); if (this.styleSheet != null) { this.styleSheet.setJStyleSheet(jSheet); } else { final JStyleSheetWrapper styleSheet = new JStyleSheetWrapper(jSheet, this.getMedia(), href, this.getType(), this.getTitle(), this, doc.styleSheetManager.bridge); this.styleSheet = styleSheet; } this.styleSheet.setDisabled(this.isAltStyleSheet() | this.disabled); doc.styleSheetManager.invalidateStyles(); } catch (final MalformedURLException mfe) { this.detachStyleSheet(); this.warn("Will not parse CSS. URI=[" + this.getHref() + "] with BaseURI=[" + doc.getBaseURI() + "] does not appear to be a valid URI."); } catch (final Exception err) { this.warn("Unable to parse CSS. URI=[" + this.getHref() + "].", err); } } } finally { doc.markJobsFinished(1, true); } } private void deferredProcess() { processLinkHelper(true); } private void processLinkHelper(final boolean defer) { // according to firefox, whenever the URL is not well formed, the style sheet has to be null // and in all other cases an empty style sheet has to be set till the link resource can be fetched // and processed. But however the style sheet is not in ready state till it is processed. This is // indicated by setting the jStyleSheet of the JStyleSheetWrapper to null. final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.getOwnerDocument(); if (isAttachedToDocument() && isWellFormedURL() && isAllowedRel() && isAllowedType()) { if (defer) { this.styleSheet = this.getEmptyStyleSheet(); doc.styleSheetManager.invalidateStyles(); //TODO need to think how to schedule this. refer issue #69 doc.addJob(() -> this.processLinkHelper(false), true); } else { processLink(); } } else { this.detachStyleSheet(); if (!defer) { doc.markJobsFinished(1, true); } } } private JStyleSheetWrapper getEmptyStyleSheet() { final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.getOwnerDocument(); return new JStyleSheetWrapper(null, this.getMedia(), this.getHref(), this.getType(), this.getTitle(), this, doc.styleSheetManager.bridge); } public CSSStyleSheet getSheet() { return this.styleSheet; } @Override protected void handleDocumentAttachmentChanged() { deferredProcess(); } @Override protected void handleAttributeChanged(final String name, final String oldValue, final String newValue) { super.handleAttributeChanged(name, oldValue, newValue); // TODO according to firefox's behavior whenever a valid attribute is // changed on the element the disabled flag is set to false. Need to // verify with the specs. // TODO check for all the attributes associated with an link element // according to firefox if the new value of rel/href is the same as the // old one then, the nothing has to be done. In all other cases the link element // has to be re-processed. if (isSameRel(name, oldValue) || isSameHref(name, oldValue)) { return; } else if ("rel".equals(name) || "href".equals(name) || "type".equals(name) || "media".equals(name)) { this.disabled = false; this.detachStyleSheet(); this.processLinkHelper(true); } } }