25

I get this error in Firebug when I try to access some CSS files hosted on external domains:

Security error" code: "1000
rules = styleSheets[i].cssRules;

The code I am using is:

$(document).ready(function () {
    $("p").live('mousedown', function getCSSRules(element) {
        element = $(this);
        var styleSheets = document.styleSheets;
        var matchedRules = [],
            rules, rule;
        for (var i = 0; i < styleSheets.length; i++) {
            rules = styleSheets[i].cssRules;
            for (var j = 0; j < rules.length; j++) {
                rule = rules[j];
                if (element.is(rule.selectorText)) {
                    matchedRules.push(rule.selectorText);
                }
            }
        }
        alert(matchedRules);
    });
});

Is there a way to fix this, beside moving all the CSS files on the same domain?

Sebastian Zartner
  • 16,477
  • 8
  • 74
  • 116
Mircea
  • 10,133
  • 21
  • 58
  • 88

7 Answers7

14

The only real solution to this problem is to CORS load your CSS in the first place. By using a CORS XMLHttpRequest to load the CSS from an external domain, and then injecting the responseText (actually responseCSS in this case) into the page via something like:

function loadCSSCors(stylesheet_uri) {
  var _xhr = global.XMLHttpRequest;
  var has_cred = false;
  try {has_cred = _xhr && ('withCredentials' in (new _xhr()));} catch(e) {}
  if (!has_cred) {
    console.error('CORS not supported');
    return;
  }
  var xhr = new _xhr();
  xhr.open('GET', stylesheet_uri);
  xhr.onload = function() {
    xhr.onload = xhr.onerror = null;
    if (xhr.status < 200 || xhr.status >= 300) {
      console.error('style failed to load: ' + stylesheet_uri);
    } else {
      var style_tag = document.createElement('style');
      style_tag.appendChild(document.createTextNode(xhr.responseText));
      document.head.appendChild(style_tag);
    }
  };
  xhr.onerror = function() {
      xhr.onload = xhr.onerror = null;
      console.error('XHR CORS CSS fail:' + styleURI);
  };
  xhr.send();
}

This way the CSS files will be interpreted by the browser as coming from the same origin domain as the main page response and now you will have access to the cssRules properties of your stylesheets.

Cattode
  • 836
  • 5
  • 10
  • Nice answer. Note however, there should be a `}` before `xhr.send()` for the function to work. – Rob Campion Jun 30 '15 at 18:12
  • global is not defined. What do I need to import to make this work? – Ulysses Alves Jul 14 '15 at 16:51
  • @UlyssesAlves try replacing global.XMLHttpRequest with either window. XMLHttpRequest or just by referencing XMLHttpRequest directly. should work for you if you're in a browser context. – Jordan M Alperin Jul 15 '15 at 17:41
  • Hey guys, I am having a very similar issue. I created a script few months back which use to run with styleSheets[i].cssRules. However, it seems that for some reason the method is now violating CORS Rules and I tried to adopt the solution above. Yet, I receive the following error: `Access to XMLHttpRequest at 'file:///C:/website/css/structure.css' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, chrome-untrusted, https.` Any idea? – Giacomo Carloni Dec 17 '20 at 13:59
10

As of 2013, you can set the "crossorigin" attribute on the <link>-Element to signal the browser that this CSS is trusted (Mozilla, W3). For this to work, the Server hosting the CSS has to set the Access-Control-Allow-Origin: * header though.

After that, you can access its rules via Javascript.

Johannes Jander
  • 4,735
  • 2
  • 28
  • 46
  • 1
    Adding `crossorigin="anonymous"` to the LINK tag works for me in Firefox 25.0.1, but not in Chrome 31, unfortunately. But thanks for the tip. – TataBlack Dec 01 '13 at 14:34
  • Contrary to Mozilla's documentation, this attribute is **not** part of the HTML5 standard at the date of this posting. This is a Firefox-specific extension. – Gili Apr 30 '14 at 10:59
  • 2
    @Gili Contrary to your statement, `crossOrigin` **is** a standardized attribute in HTML5 ([since November 2012](http://lists.whatwg.org/pipermail/commit-watchers-whatwg.org/2012/006700.html)), and it is not only implemented in Firefox, but also Chrome (since version 25.0.1318.0 (=November 2012), [crbug.com/178787](http://code.google.com/p/chromium/issues/detail?id=178787)) and Opera 15. – Rob W Aug 01 '14 at 13:30
  • @RobW You are probably right but this isn't so clear cut. http://www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-link-element contains the `crossOrigin` property but http://www.w3.org/TR/html5/links.html does not. WhatWG is a "living standard" but practice this means that properties come and go over time. I find it hard to refer to this as the "standard". http://www.i-programmer.info/news/191-htmlcss/4531-which-html5-whatwg-and-w3c-split.html discusses this in more detail. In short: Chrome and Firefox support this, but until W3C does it's unlikely IE will. – Gili Aug 04 '14 at 10:03
  • 2
    @Gill You're looking at the wrong part of the specification. http://www.w3.org/TR/html5/links.html specifies the concept of a "link", not the `` element. `` is mentioned in the specs of both WHATWG and W3 http://www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-link-element and http://www.w3.org/TR/html5/document-metadata.html#the-link-element – Rob W Aug 04 '14 at 10:04
  • @RobW Agreed. Thanks for the clarification! – Gili Aug 17 '14 at 18:50
2

I wrote a little function that will solve the loading problem cross-browser including FF. The comments on GitHub help explain usage. Full code at https://github.com/srolfe26/getXDomainCSS.

Disclaimer: The code below is jQuery dependent.

Sometimes, if you're pulling CSS from a place that you can't control the CORS settings you cans till get the CSS with an <link> tag, the main issue to be solved then becomes knowing when your called-for CSS has been loaded and ready to use. In older IE, you could have an on_load listener run when the CSS is loaded.

Newer browsers seem to require old-fashioned polling to determine when the file is loaded, and have some cross-browser issues in determining when the load is satisfied. See the code below to catch some of those quirks.

/**
 * Retrieves CSS files from a cross-domain source via javascript. Provides a jQuery implemented
 * promise object that can be used for callbacks for when the CSS is actually completely loaded.
 * The 'onload' function works for IE, while the 'style/cssRules' version works everywhere else
 * and accounts for differences per-browser.
 *
 * @param   {String}    url     The url/uri for the CSS file to request
 * 
 * @returns {Object}    A jQuery Deferred object that can be used for 
 */
function getXDomainCSS(url) {
    var link,
        style,
        interval,
        timeout = 60000,                        // 1 minute seems like a good timeout
        counter = 0,                            // Used to compare try time against timeout
        step = 30,                              // Amount of wait time on each load check
        docStyles = document.styleSheets        // local reference
        ssCount = docStyles.length,             // Initial stylesheet count
        promise = $.Deferred();

    // IE 8 & 9 it is best to use 'onload'. style[0].sheet.cssRules has problems.
    if (navigator.appVersion.indexOf("MSIE") != -1) {
        link = document.createElement('link');
        link.type = "text/css";
        link.rel = "stylesheet";
        link.href = url;

        link.onload = function () {
            promise.resolve();
        }

        document.getElementsByTagName('head')[0].appendChild(link);
    }

    // Support for FF, Chrome, Safari, and Opera
    else {
        style = $('<style>')
            .text('@import "' + url + '"')
            .attr({
                 // Adding this attribute allows the file to still be identified as an external
                 // resource in developer tools.
                 'data-uri': url
            })
            .appendTo('body');

        // This setInterval will detect when style rules for our stylesheet have loaded.
        interval = setInterval(function() {
            try {
                // This will fail in Firefox (and kick us to the catch statement) if there are no 
                // style rules.
                style[0].sheet.cssRules;

                // The above statement will succeed in Chrome even if the file isn't loaded yet
                // but Chrome won't increment the styleSheet length until the file is loaded.
                if(ssCount === docStyles.length) {
                    throw(url + ' not loaded yet');
                }
                else {
                    var loaded = false,
                        href,
                        n;

                    // If there are multiple files being loaded at once, we need to make sure that 
                    // the new file is this file
                    for (n = docStyles.length - 1; n >= 0; n--) {
                        href = docStyles[n].cssRules[0].href;

                        if (typeof href != 'undefined' && href === url) {
                            // If there is an HTTP error there is no way to consistently
                            // know it and handle it. The file is considered 'loaded', but
                            // the console should will the HTTP error.
                            loaded = true;
                            break;
                        }
                    }

                    if (loaded === false) {
                        throw(url + ' not loaded yet');
                    }
                }

                // If an error wasn't thrown by this point in execution, the stylesheet is loaded, proceed.
                promise.resolve();
                clearInterval(interval);
            } catch (e) {
                counter += step;

                if (counter > timeout) {
                    // Time out so that the interval doesn't run indefinitely.
                    clearInterval(interval);
                    promise.reject();
                }

            }
        }, step);   
    }

    return promise;
}
srolfe26
  • 79
  • 7
  • Link-only answers are generally [frowned upon](http://meta.stackexchange.com/a/8259/204922) on Stack Overflow. In time it is possible for links to atrophy and become unavailable, meaning that your answer is useless to users in the future. It would be best if you could provide the general details of your answer in your actual post, citing your link as a reference. – vaultah Apr 29 '16 at 18:10
  • 1
    @vaultah Thanks for the best-practices advice. Only took me a year to get around to updating it :-) Cheers. – srolfe26 Mar 24 '17 at 21:51
2

If you have control over the domain where the external stylesheet is hosted, it may help to add an appropriate Access-Control-Allow-Origin header.

Access-Control-Allow-Origin: http://stylesheet-user.example.com
gerrit
  • 1,539
  • 14
  • 13
1

I had a similar issue under firefox and chrome. I've solved it in a harsh way by adding to my domain a css file which included the external domain css, like this:

<style type="text/css">
@import url("https://externaldomain.com/includes/styles/cookie-btn.css");
</style>

It's fast but dirty. It's recommended to keep all css files in your domain.

Radek Busz
  • 314
  • 4
  • 11
  • 1
    It does not work for me. Tested on this very page that (now) has external stylesheets. `var style = $(''); var importRule = style.sheet.cssRules[0]; var cssRules = importRule.styleSheet.cssRules; // null!` – Georgii Ivankin Mar 27 '13 at 16:57
  • Sure you could do that. But why then pair that with `@import`? – alex May 09 '13 at 02:20
  • import produce Access-Control-Allow-Origin – fdrv Mar 31 '16 at 21:11
1

If this triggers for you because some of your CSS may come from elsewhere but NOT the bit you are interested in, use a try... catch block like this:

function cssAttributeGet(selectorText,attribute) {
  var styleSheet, rules, i, ii;
  selectorText=selectorText.toLowerCase();
  if (!document.styleSheets) {
    return false;
  }
  for (i=0; i<document.styleSheets.length; i++) {
    try{
      styleSheet=document.styleSheets[i];
      rules = (styleSheet.cssRules ? styleSheet.cssRules : styleSheet.rules);
      for (ii=0; ii<rules.length; ii++) {
        if (
          rules[ii] && rules[ii].selectorText &&
          rules[ii].selectorText.toLowerCase()===selectorText &&
          rules[ii].style[attribute]
        ){
          return (rules[ii].style[attribute]);
        }
      }
    }
    catch(e){
      // Do nothing!
    };
  }
  return false;
}
alex
  • 438,662
  • 188
  • 837
  • 957
0

You may have to write a proxy for that CSS-file.

Peter Örneholm
  • 2,831
  • 18
  • 24