Skip to content

Commit

Permalink
Merge pull request #63 from NeuromatchAcademy/feature-myspace
Browse files Browse the repository at this point in the history
Myspace Mode - Chapter 1: User Custom CSS
  • Loading branch information
sneakers-the-rat authored Sep 22, 2024
2 parents d416ae6 + f11b82e commit b6b3324
Show file tree
Hide file tree
Showing 18 changed files with 118 additions and 17 deletions.
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

0 comments on commit b6b3324

Please sign in to comment.