sourceafDuvet::LinkTagBuilder.fan

using afIoc::Inject
using afIocConfig::Config
using afBedSheet

** Defines a '<link>' tag to be injected into the bottom of your head. Autobuild or create via `HtmlInjector`.
** 
** If defining a stylesheet, note that any 'Content-Security-Policy' response header will be updated to ensure it can be loaded.
** 
** @see `https://developer.mozilla.org/en/docs/Web/HTML/Element/link` 
class LinkTagBuilder {
    
    @Inject private HttpResponse        httpRes
    @Inject private ClientAssetCache    clientAssets
    @Inject { optional=true }           // nullable for testing
            private BedSheetServer?     bedServer
    @Inject { optional=true }           // nullable for testing
            private FileHandler?        fileHandler
            private HtmlElement         element
            private HtmlConditional     ieConditional
    @Config private Bool                updateCspHeader

    internal new make(|This|in) {
        in(this)
        this.element = HtmlElement("link")
        ieConditional = HtmlConditional() { element, }
    }
    
    ** Sets the 'href' attribute to an external URL.
    ** Returns 'this'.
    LinkTagBuilder fromExternalUrl(Uri externalUrl) {
        if (externalUrl.host == null)
            throw ArgErr(ErrMsgs.externalUrlsNeedHost(externalUrl))
        element["href"] = externalUrl.encode
        
        if (updateCsp) {
            host := (externalUrl + `/`).encode
            if (host.endsWith("/"))
                host = host[0..<-1]
            if (host.startsWith("//"))
                host = host[2..-1]
            csp  := httpRes.headers.contentSecurityPolicy
            if (addCsp(csp, host))
                httpRes.headers.contentSecurityPolicy = csp

            cspro := httpRes.headers.contentSecurityPolicyReportOnly
            if (addCsp(cspro, host))
                httpRes.headers.contentSecurityPolicyReportOnly = cspro
        }

        return this
    }

    ** Sets the 'href' attribute to a local URL. 
    ** The URL **must** be mapped by BedSheet's 'ClientAsset' cache service.
    ** The URL may be rebuilt to take advantage of any asset caching strategies, such as [Cold Feet]`http://eggbox.fantomfactory.org/pods/afColdFeet`.
    ** Returns 'this'.
    LinkTagBuilder fromLocalUrl(Uri localUrl) {
        // ClientAssets adds ColdFeet digests, BedServer does not
        clientUrl := clientAssets.getAndUpdateOrProduce(localUrl)?.clientUrl ?: bedServer.toClientUrl(localUrl) 
        element["href"] = clientUrl.encode
        
        if (updateCsp) {
            csp := httpRes.headers.contentSecurityPolicy
            if (addCsp(csp, "'self'"))
                httpRes.headers.contentSecurityPolicy = csp

            cspro := httpRes.headers.contentSecurityPolicyReportOnly
            if (addCsp(cspro, "'self'"))
                httpRes.headers.contentSecurityPolicyReportOnly = cspro
        }
        
        return this     
    }

    ** Creates a 'href' URL attribute from the given file. 
    ** The file **must** exist on the file system and be mapped by BedSheet's 'FileHandler' service.
    ** The URL is built to take advantage of any asset caching strategies, such as [Cold Feet]`http://eggbox.fantomfactory.org/pods/afColdFeet`.
    ** Returns 'this'.
    LinkTagBuilder fromServerFile(File serverFile) {
        fileAsset := fileHandler.fromServerFile(serverFile, true)   // this add any ColdFeet digests
        element["href"] = fileAsset.clientUrl.encode
        
        if (updateCsp) {
            csp := httpRes.headers.contentSecurityPolicy
            if (addCsp(csp, "'self'"))
                httpRes.headers.contentSecurityPolicy = csp

            cspro := httpRes.headers.contentSecurityPolicyReportOnly
            if (addCsp(cspro, "'self'"))
                httpRes.headers.contentSecurityPolicyReportOnly = cspro
        }

        return this     
    }
    
    ** Sets the 'type' attribute.
    ** Returns 'this'.
    LinkTagBuilder withType(MimeType type) {
        element["type"] = type.toStr
        return this
    }
    
    ** Sets the 'media' attribute.
    ** Returns 'this'.
    LinkTagBuilder withMedia(Str media) {
        element["media"] = media
        return this
    }
    
    ** Wraps the '<link>' element in a [conditional IE comment]`http://www.quirksmode.org/css/condcom.html`. 
    ** The given 'condition' should be everything in the square brackets. Example:
    **  
    **   ifIe("if gt IE 6")
    ** 
    ** would render:
    ** 
    **   <!--[if gt IE 6]>
    **     <link src="..." >
    **   <![endif]-->
    LinkTagBuilder ifIe(Str condition) {
        ieConditional.condition = condition 
        return this
    }
    
    ** Sets the 'rel' attribute.
    ** Returns 'this'.
    LinkTagBuilder withRel(Str rel) {
        element["rel"] = rel
        return this
    }
    
    ** Sets the 'title' attribute.
    ** Returns 'this'.
    LinkTagBuilder withTitle(Str title) {
        element["title"] = title
        return this
    }
    
    ** Sets an arbitrary attribute on the '<link>' element. 
    LinkTagBuilder setAttr(Str name, Str value) {
        element[name] = value
        return this
    }
    
    ** Returns an attribute value on the '<link>' element. 
    Str? getAttr(Str name) {
        element[name]
    }   

    @NoDoc  // looks like it could be useful!
    HtmlNode htmlNode() {
        ieConditional
    }
    
    private Bool updateCsp() {
        if (!updateCspHeader) return false 
        type := MimeType(element["type"] ?: "", false)?.noParams?.toStr
        rel  := element["rel"]
        return type == "text/css" || rel == "stylesheet" 
    }
    
    private static Bool addCsp([Str:Str]? csp, Str newDir) {
        if (csp == null)
            return false

        directive   := csp["style-src"]?.trimToNull ?: csp["default-src"]?.trimToNull
        if (directive == null)
            return false

        directives  := directive.split
        if (directives.contains(newDir))    // e.g. 'self'
            return false
        
        csp["style-src"] = directive + " " + newDir
        return true
    }
}