Skip to content

Commit

Permalink
SECURITY: Generate more category CSS on client
Browse files Browse the repository at this point in the history
This commit moves the generation of category background CSS from the
server side to the client side. This simplifies the server side code
because it does not need to check which categories are visible to the
current user.
  • Loading branch information
nbianca authored and nattsw committed Mar 15, 2024
1 parent 62ea382 commit b425fbc
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 209 deletions.
@@ -0,0 +1,55 @@
import { getURLWithCDN } from "discourse-common/lib/get-url";

export default {
after: "register-hashtag-types",

initialize(owner) {
this.session = owner.lookup("service:session");
this.site = owner.lookup("service:site");

if (!this.site.categories?.length) {
return;
}

const css = [];
const darkCss = [];

this.site.categories.forEach((category) => {
const lightUrl = category.uploaded_background?.url;
const darkUrl =
this.session.defaultColorSchemeIsDark || this.session.darkModeAvailable
? category.uploaded_background_dark?.url
: null;
const defaultUrl =
darkUrl && this.session.defaultColorSchemeIsDark ? darkUrl : lightUrl;

if (defaultUrl) {
const url = getURLWithCDN(defaultUrl);
css.push(
`body.category-${category.fullSlug} { background-image: url(${url}); }`
);
}

if (darkUrl && defaultUrl !== darkUrl) {
const url = getURLWithCDN(darkUrl);
darkCss.push(
`body.category-${category.fullSlug} { background-image: url(${url}); }`
);
}
});

if (darkCss.length > 0) {
css.push("@media (prefers-color-scheme: dark) {", ...darkCss, "}");
}

const cssTag = document.createElement("style");
cssTag.type = "text/css";
cssTag.id = "category-background-css-generator";
cssTag.innerHTML = css.join("\n");
document.head.appendChild(cssTag);
},

teardown() {
document.querySelector("#category-background-css-generator")?.remove();
},
};
@@ -0,0 +1,132 @@
import { visit } from "@ember/test-helpers";
import { test } from "qunit";
import Session from "discourse/models/session";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";

const SITE_DATA = {
categories: [
{
id: 1,
color: "ff0000",
text_color: "ffffff",
name: "category1",
slug: "foo",
uploaded_background: {
id: 54,
url: "/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png",
width: 1024,
height: 768,
},
},
{
id: 2,
color: "333",
text_color: "ffffff",
name: "category2",
slug: "bar",
uploaded_background_dark: {
id: 25,
url: "/uploads/default/original/1X/f9fdb0ad108f2aed178c40f351bbb2c7cb2571e3.png",
width: 1024,
height: 768,
},
},
{
id: 4,
color: "2B81AF",
text_color: "ffffff",
parent_category_id: 1,
name: "category3",
slug: "baz",
uploaded_background: {
id: 11,
url: "/uploads/default/original/1X/684c104edc18a7e9cef1fa31f41215f3eec5d92b.png",
width: 1024,
height: 768,
},
uploaded_background_dark: {
id: 19,
url: "/uploads/default/original/1X/89b1a2641e91604c32b21db496be11dba7a253e6.png",
width: 1024,
height: 768,
},
},
],
};

acceptance("Category Background CSS Generator", function (needs) {
needs.user();
needs.site(SITE_DATA);

test("CSS classes are generated", async function (assert) {
await visit("/");

assert.equal(
document.querySelector("#category-background-css-generator").innerHTML,
"body.category-foo { background-image: url(/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png); }\n" +
"body.category-foo-baz { background-image: url(/uploads/default/original/1X/684c104edc18a7e9cef1fa31f41215f3eec5d92b.png); }"
);
});
});

acceptance("Category Background CSS Generator (dark)", function (needs) {
needs.user();
needs.site(SITE_DATA);

needs.hooks.beforeEach(function () {
const session = Session.current();
session.set("darkModeAvailable", true);
session.set("defaultColorSchemeIsDark", false);
});

needs.hooks.afterEach(function () {
const session = Session.current();
session.set("darkModeAvailable", null);
session.set("defaultColorSchemeIsDark", null);
});

test("CSS classes are generated", async function (assert) {
await visit("/");

assert.equal(
document.querySelector("#category-background-css-generator").innerHTML,
"body.category-foo { background-image: url(/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png); }\n" +
"body.category-foo-baz { background-image: url(/uploads/default/original/1X/684c104edc18a7e9cef1fa31f41215f3eec5d92b.png); }\n" +
"@media (prefers-color-scheme: dark) {\n" +
"body.category-bar { background-image: url(/uploads/default/original/1X/f9fdb0ad108f2aed178c40f351bbb2c7cb2571e3.png); }\n" +
"body.category-foo-baz { background-image: url(/uploads/default/original/1X/89b1a2641e91604c32b21db496be11dba7a253e6.png); }\n" +
"}"
);
});
});

acceptance(
"Category Background CSS Generator (dark is default)",
function (needs) {
needs.user();
needs.site(SITE_DATA);

needs.hooks.beforeEach(function () {
const session = Session.current();
session.set("darkModeAvailable", true);
session.set("defaultColorSchemeIsDark", true);
});

needs.hooks.afterEach(function () {
const session = Session.current();
session.set("darkModeAvailable", null);
session.set("defaultColorSchemeIsDark", null);
});

test("CSS classes are generated", async function (assert) {
await visit("/");

assert.equal(
document.querySelector("#category-background-css-generator").innerHTML,
"body.category-foo { background-image: url(/uploads/default/original/1X/c5c84b16ebf745ab848d1498267c541facbf1ff0.png); }\n" +
"body.category-bar { background-image: url(/uploads/default/original/1X/f9fdb0ad108f2aed178c40f351bbb2c7cb2571e3.png); }\n" +
"body.category-foo-baz { background-image: url(/uploads/default/original/1X/89b1a2641e91604c32b21db496be11dba7a253e6.png); }"
);
});
}
);
1 change: 0 additions & 1 deletion lib/stylesheet/compiler.rb
Expand Up @@ -34,7 +34,6 @@ def self.compile_asset(asset, options = {})
when Stylesheet::Manager::COLOR_SCHEME_STYLESHEET
file += importer.import_color_definitions
file += importer.import_wcag_overrides
file += importer.category_backgrounds(options[:color_scheme_id])
file += importer.font
end
end
Expand Down
30 changes: 0 additions & 30 deletions lib/stylesheet/importer.rb
Expand Up @@ -95,20 +95,6 @@ def wizard_fonts
contents
end

def category_backgrounds(color_scheme_id)
is_dark_color_scheme =
color_scheme_id.present? && ColorScheme.find_by_id(color_scheme_id)&.is_dark?

contents = +""
Category
.where("uploaded_background_id IS NOT NULL")
.each do |c|
contents << category_css(c, is_dark_color_scheme) if c.uploaded_background&.url.present?
end

contents
end

def import_color_definitions
contents = +""
DiscoursePluginRegistry.color_definition_stylesheets.each do |name, path|
Expand Down Expand Up @@ -220,22 +206,6 @@ def theme
@theme == :nil ? nil : @theme
end

def category_css(category, is_dark_color_scheme)
full_slug = category.full_slug.split("-")[0..-2].join("-")

# in case we're using a dark color scheme, we define the background using the dark image
# if one is available. Otherwise, we use the light image by default.
if is_dark_color_scheme && category.uploaded_background_dark&.url.present?
return category_background_css(full_slug, category.uploaded_background_dark.url)
end

category_background_css(full_slug, category.uploaded_background.url)
end

def category_background_css(full_slug, background_url)
"body.category-#{full_slug} { background-image: url(#{upload_cdn_path(background_url)}) }"
end

def font_css(font)
contents = +""

Expand Down
2 changes: 1 addition & 1 deletion lib/stylesheet/manager.rb
Expand Up @@ -7,7 +7,7 @@ module Stylesheet
end

class Stylesheet::Manager
BASE_COMPILER_VERSION = 1
BASE_COMPILER_VERSION = 2

CACHE_PATH = "tmp/stylesheet-cache"
private_constant :CACHE_PATH
Expand Down
11 changes: 2 additions & 9 deletions lib/stylesheet/manager/builder.rb
Expand Up @@ -240,20 +240,13 @@ def default_digest
def color_scheme_digest
cs = @color_scheme || theme&.color_scheme

categories_updated =
Stylesheet::Manager
.cache
.defer_get_set("categories_updated") do
Category.where("uploaded_background_id IS NOT NULL").pluck(:updated_at).map(&:to_i).sum
end

fonts = "#{SiteSetting.base_font}-#{SiteSetting.heading_font}"

digest_string = "#{current_hostname}-"
if cs || categories_updated > 0
if cs
theme_color_defs = resolve_baked_field(:common, :color_definitions)
digest_string +=
"#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.fs_asset_cachebuster}-#{categories_updated}-#{fonts}"
"#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}"
else
digest_string += "defaults-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}"

Expand Down

0 comments on commit b425fbc

Please sign in to comment.