Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Myspace Mode - Chapter 1: User Custom CSS #63

Merged
merged 15 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.vagrant
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ DB_HOST=/var/run/postgresql/

ES_ENABLED=true
ES_HOST=localhost
ES_PORT=9200
ES_PORT=9200
2 changes: 1 addition & 1 deletion app/controllers/settings/profiles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def update
private

def account_params
params.require(:account).permit(:display_name, :note, :avatar, :header, :bot, fields_attributes: [:name, :value])
params.require(:account).permit(:display_name, :note, :avatar, :header, :bot, :account_css, fields_attributes: [:name, :value])
end

def set_account
Expand Down
1 change: 1 addition & 0 deletions app/javascript/flavours/glitch/api_types/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ export interface ApiAccountJSON {
limited?: boolean;
memorial?: boolean;
hide_collections: boolean;
account_css?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';

import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { withRouter } from 'react-router-dom';
import {Helmet} from 'react-helmet';
import {withRouter} from 'react-router-dom';

import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
Expand Down Expand Up @@ -405,6 +405,11 @@ class Header extends ImmutablePureComponent {
<title>{titleFromAccount(account)}</title>
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
<link rel='canonical' href={account.get('url')} />
{account.account_css && (
<style id={"account-css"} nonce={document.querySelector('meta[name=style-nonce]').content}>
{account.account_css}
</style>
)}
</Helmet>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions app/javascript/flavours/glitch/models/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const accountDefaultValues: AccountShape = {
limited: false,
moved: null,
hide_collections: false,
account_css: '',
};

const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import 'latex';
@import 'bigger_collapsed_statuses';
@import 'better_code_blocks';
@import 'myspace';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#account_account_css {
font-family: $font-monospace;
height: 15em;
}
4 changes: 3 additions & 1 deletion app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@
# avatar_storage_schema_version :integer
# header_storage_schema_version :integer
# devices_url :string
# suspension_origin :integer
# sensitized_at :datetime
# suspension_origin :integer
# trendable :boolean
# reviewed_at :datetime
# requested_review_at :datetime
# indexable :boolean default(FALSE), not null
# account_css :text
#

class Account < ApplicationRecord
Expand Down Expand Up @@ -114,6 +115,7 @@ class Account < ApplicationRecord
validates :followers_url, absence: true, if: :local?, on: :create

normalizes :username, with: ->(username) { username.squish }
normalizes :account_css, with: ->(account_css) { Sanitize::CSS.stylesheet(account_css, Sanitize::Config::RELAXED) }

scope :without_internal, -> { where(id: 1...) }
scope :remote, -> { where.not(domain: nil) }
Expand Down
3 changes: 2 additions & 1 deletion app/serializers/rest/account_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ class REST::AccountSerializer < ActiveModel::Serializer

attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :indexable, :group, :created_at,
:note, :url, :uri, :avatar, :avatar_static, :header, :header_static,
:followers_count, :following_count, :statuses_count, :last_status_at, :hide_collections
:followers_count, :following_count, :statuses_count, :last_status_at, :hide_collections,
:account_css

has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?

Expand Down
5 changes: 5 additions & 0 deletions app/views/settings/profiles/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,10 @@
.fields-group
= f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot')

%h4= t 'edit_profile.myspace_mode'

.fields-group
= f.input :account_css, as: :text, wrapper: :with_block_label, label: I18n.t('simple_form.labels.account.account_css'), hint: I18n.t('simple_form.hints.account.account_css'), neuromatchstodon_only: true

.actions
= f.button :button, t('generic.save_changes'), type: :submit
10 changes: 10 additions & 0 deletions config/initializers/simple_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,20 @@ def glitch_only(_wrapper_options = nil)
end
end

module NeuromatchstodonOnlyComponent
def neuromatchstodon_only(_wrapper_options = nil)
return unless options[:neuromatchstodon_only]

options[:label_text] = ->(raw_label_text, _required_label_text, _label_present) { safe_join([raw_label_text, ' ', content_tag(:span, I18n.t('simple_form.neuromatchstodon_only'), class: 'glitch_only')]) }
nil
end
end

SimpleForm.include_component(AppendComponent)
SimpleForm.include_component(RecommendedComponent)
SimpleForm.include_component(WarningHintComponent)
SimpleForm.include_component(GlitchOnlyComponent)
SimpleForm.include_component(NeuromatchstodonOnlyComponent)

SimpleForm.setup do |config|
# Wrappers are used by the form builder to generate a
Expand Down
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,7 @@ en:
basic_information: Basic information
hint_html: "<strong>Customize what people see on your public profile and next to your posts.</strong> Other people are more likely to follow you back and interact with you when you have a filled out profile and a profile picture."
other: Other
myspace_mode: "MySpace Mode"
errors:
'400': The request you submitted was invalid or malformed.
'403': You don't have permission to view this page.
Expand Down
3 changes: 3 additions & 0 deletions config/locales/simple_form.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ en:
note: 'You can @mention other people or #hashtags.'
show_collections: People will be able to browse through your follows and followers. People that you follow will see that you follow them regardless.
unlocked: People will be able to follow you without requesting approval. Uncheck if you want to review follow requests and chose whether to accept or reject new followers.
account_css: Custom CSS that is applied to your account page
account_alias:
acct: Specify the username@domain of the account you want to move from
account_migration:
Expand Down Expand Up @@ -150,6 +151,7 @@ en:
indexable: Include public posts in search results
show_collections: Show follows and followers on profile
unlocked: Automatically accept new followers
account_css: Account CSS
account_alias:
acct: Handle of the old account
account_migration:
Expand Down Expand Up @@ -339,3 +341,4 @@ en:
sessions:
webauthn: Use one of your security keys to sign in
'yes': 'Yes'
neuromatchstodon_only: "Neuromatchstodon Only"
7 changes: 7 additions & 0 deletions db/migrate/20240828084252_add_account_css_to_account.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddAccountCssToAccount < ActiveRecord::Migration[7.1]
def change
add_column :accounts, :account_css, :text, null: true
end
end
21 changes: 11 additions & 10 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.1].define(version: 2024_08_08_125420) do
ActiveRecord::Schema[7.1].define(version: 2024_08_28_084252) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

Expand Down Expand Up @@ -200,6 +200,7 @@
t.datetime "reviewed_at", precision: nil
t.datetime "requested_review_at", precision: nil
t.boolean "indexable", default: false, null: false
t.text "account_css"
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
t.index ["domain", "id"], name: "index_accounts_on_domain_and_id"
Expand Down Expand Up @@ -1428,9 +1429,9 @@
add_index "instances", ["domain"], name: "index_instances_on_domain", unique: true

create_view "user_ips", sql_definition: <<-SQL
SELECT user_id,
ip,
max(used_at) AS used_at
SELECT t0.user_id,
t0.ip,
max(t0.used_at) AS used_at
FROM ( SELECT users.id AS user_id,
users.sign_up_ip AS ip,
users.created_at AS used_at
Expand All @@ -1447,7 +1448,7 @@
login_activities.created_at
FROM login_activities
WHERE (login_activities.success = true)) t0
GROUP BY user_id, ip;
GROUP BY t0.user_id, t0.ip;
SQL
create_view "account_summaries", materialized: true, sql_definition: <<-SQL
SELECT accounts.id AS account_id,
Expand All @@ -1468,9 +1469,9 @@
add_index "account_summaries", ["account_id"], name: "index_account_summaries_on_account_id", unique: true

create_view "global_follow_recommendations", materialized: true, sql_definition: <<-SQL
SELECT account_id,
sum(rank) AS rank,
array_agg(reason) AS reason
SELECT t0.account_id,
sum(t0.rank) AS rank,
array_agg(t0.reason) AS reason
FROM ( SELECT account_summaries.account_id,
((count(follows.id))::numeric / (1.0 + (count(follows.id))::numeric)) AS rank,
'most_followed'::text AS reason
Expand All @@ -1494,8 +1495,8 @@
WHERE (follow_recommendation_suppressions.account_id = statuses.account_id)))))
GROUP BY account_summaries.account_id
HAVING (sum((status_stats.reblogs_count + status_stats.favourites_count)) >= (5)::numeric)) t0
GROUP BY account_id
ORDER BY (sum(rank)) DESC;
GROUP BY t0.account_id
ORDER BY (sum(t0.rank)) DESC;
SQL
add_index "global_follow_recommendations", ["account_id"], name: "index_global_follow_recommendations_on_account_id", unique: true

Expand Down
27 changes: 27 additions & 0 deletions spec/controllers/settings/profiles_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,31 @@
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
end
end

describe 'PUT #account_css with custom css' do
it 'hopefully removes malicious css' do
put :update, params: {
account: {
account_css: <<~CSS,
@import url(swear_words.css);
a { text-decoration: none; }

a:hover {
left: expression(alert('xss!'));
text-decoration: underline;
}
CSS
},
}
expect(account.reload.account_css).to eq <<~CSS

a { text-decoration: none; }

a:hover {

text-decoration: underline;
}
CSS
end
end
end
17 changes: 16 additions & 1 deletion spec/support/stories/profile_stories.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module ProfileStories
attr_reader :bob, :alice, :alice_bio
attr_reader :bob, :alice, :alice_bio, :chupacabra, :chupacabra_css

def fill_in_auth_details(email, password)
fill_in 'user_email', with: email
Expand Down Expand Up @@ -43,6 +43,21 @@ def with_alice_as_local_user
)
end

def with_chupacabras_fancy_profile
@chupacabra_css = <<~CSS
body {
background-color: red !important;
}
CSS

@chupacabra = Fabricate(
:user,
email: '[email protected]', password: password, confirmed_at: confirmed_at,
account: Fabricate(:account, username: 'chupacabra', note: 'I am gonna getcha!', account_css: @chupacabra_css)
)
Web::Setting.where(user: chupacabra).first_or_initialize(user: chupacabra).update!(data: { introductionVersion: 2018_12_16_044202 })
end

def confirmed_at
@confirmed_at ||= Time.zone.now
end
Expand Down
17 changes: 17 additions & 0 deletions spec/system/profile_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,21 @@

expect(subject).to have_content 'Changes successfully saved!'
end

describe 'with JS', :js, :streaming do
before do
with_chupacabras_fancy_profile
end

it 'Can have custom account_css set' do
visit account_path('chupacabra')
# wait for page to load...
page.find '.account__header'
expect(subject.html).to have_content('background-color: red !important')

visit account_path('bob')
page.find '.account__header'
expect(subject.html).to have_no_content('background-color: red !important')
end
end
end
Loading