diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 0ddf1f9184c..c8a0d0a8e33 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -66,7 +66,7 @@ def params_user end end - PERMITTED_PROFILE_PARAMS = %i[handle twitter_username unconfirmed_email public_email full_name].freeze + PERMITTED_PROFILE_PARAMS = %i[handle twitter_username unconfirmed_email homepage_url public_email full_name].freeze def verify_password password = params.expect(user: :password)[:password] diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 510094d00a4..5a6ab29b323 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -27,6 +27,7 @@ def create password website twitter_username + homepage_url full_name ].freeze diff --git a/app/helpers/url_helper.rb b/app/helpers/url_helper.rb new file mode 100644 index 00000000000..da941b06848 --- /dev/null +++ b/app/helpers/url_helper.rb @@ -0,0 +1,7 @@ +module UrlHelper + def display_safe_url(url) + return "" if url.blank? + return h(url) if url.start_with?("https://", "http://") + "https://#{h(url)}" + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 3741b4c7003..38cdd54e9f7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -83,6 +83,8 @@ class User < ApplicationRecord message: "can only contain letters, numbers, and underscores" }, allow_nil: true, length: { within: 0..20 } + validates :homepage_url, http_url: true, allow_blank: true + validates :password, length: { minimum: 10 }, unpwn: true, @@ -352,7 +354,7 @@ def clear_personal_attributes handle: nil, email_confirmed: false, unconfirmed_email: nil, blocked_email: nil, api_key: nil, confirmation_token: nil, remember_token: nil, - twitter_username: nil, webauthn_id: nil, full_name: nil, + twitter_username: nil, webauthn_id: nil, full_name: nil, homepage_url: nil, totp_seed: nil, mfa_hashed_recovery_codes: nil, mfa_level: :disabled, password: SecureRandom.hex(20).encode("UTF-8") diff --git a/app/views/dashboards/_subject.html.erb b/app/views/dashboards/_subject.html.erb index b7efadc3565..c0921ff72e9 100644 --- a/app/views/dashboards/_subject.html.erb +++ b/app/views/dashboards/_subject.html.erb @@ -21,6 +21,20 @@ <% end %> + <% if user.homepage_url.present? %> +
+ <%= icon_tag("link", color: :primary, class: "w-6 text-orange mr-3") %> +

<%= + link_to( + truncate(display_safe_url(user.homepage_url), length: 20), + display_safe_url(user.homepage_url), + rel: "nofollow", + data: { confirm: "You are about to be redirected #{display_safe_url(user.homepage_url)}" } + ) + %>

+
+ <% end %> + <% if user.twitter_username.present? %>
<%= icon_tag("x-twitter", color: :primary, class: "w-6 text-orange mr-3") %> diff --git a/app/views/profiles/edit.html.erb b/app/views/profiles/edit.html.erb index 9328c5ba61d..38251ff3d9d 100644 --- a/app/views/profiles/edit.html.erb +++ b/app/views/profiles/edit.html.erb @@ -20,6 +20,11 @@ <%= form.text_field :handle, :class => 'form__input' %>
+
+ <%= form.label :homepage_url, class: 'form__label' %> + <%= form.text_field(:homepage_url, class: 'form__input') %> +
+
<%= form.label :twitter_username, class: 'form__label form__label__icon-container' do %> <%= diff --git a/app/views/profiles/show.html.erb b/app/views/profiles/show.html.erb index a109e68018b..fbbb36dd0d2 100644 --- a/app/views/profiles/show.html.erb +++ b/app/views/profiles/show.html.erb @@ -94,6 +94,19 @@ ) %> <% end %> + <% if @user.homepage_url.present? %> +

+ <%= + link_to( + truncate(display_safe_url(@user.homepage_url),length: 20), + display_safe_url(@user.homepage_url), + rel: "nofollow", + class: "profile__header__attribute t-link--black", + data: { confirm: "You are about to be redirected #{display_safe_url(@user.homepage_url)} " } + ) + %> +

+ <% end %> <% end %>
diff --git a/db/migrate/20241114211431_add_homepage_url_to_users.rb b/db/migrate/20241114211431_add_homepage_url_to_users.rb new file mode 100644 index 00000000000..167d4fad68b --- /dev/null +++ b/db/migrate/20241114211431_add_homepage_url_to_users.rb @@ -0,0 +1,5 @@ +class AddHomepageUrlToUsers < ActiveRecord::Migration[7.2] + def change + add_column :users, :homepage_url, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 8d0190f2dac..f42efe7ab54 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_11_04_065953) do +ActiveRecord::Schema[7.2].define(version: 2024_11_14_211431) do # These are extensions that must be enabled in order to support this database enable_extension "hstore" enable_extension "pgcrypto" @@ -557,6 +557,7 @@ t.string "mfa_hashed_recovery_codes", default: [], array: true t.boolean "public_email", default: false, null: false t.datetime "deleted_at" + t.string "homepage_url" t.index "lower((email)::text) varchar_pattern_ops", name: "index_users_on_lower_email" t.index ["email"], name: "index_users_on_email" t.index ["handle"], name: "index_users_on_handle" diff --git a/lib/http_url_validator.rb b/lib/http_url_validator.rb new file mode 100644 index 00000000000..258772a9dee --- /dev/null +++ b/lib/http_url_validator.rb @@ -0,0 +1,8 @@ +class HttpUrlValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + uri = URI::DEFAULT_PARSER.parse(value) + record.errors.add attribute, "is not a valid URL" unless [URI::HTTP, URI::HTTPS].member?(uri.class) + rescue URI::InvalidURIError + record.errors.add attribute, "is not a valid URL" + end +end diff --git a/test/integration/profile_test.rb b/test/integration/profile_test.rb index 8585cfed4ad..1204ab2b2a9 100644 --- a/test/integration/profile_test.rb +++ b/test/integration/profile_test.rb @@ -133,6 +133,21 @@ def sign_out assert page.has_link?("@nick1", href: "https://twitter.com/nick1") end + test "adding homepage url" do + sign_in + visit profile_path("nick1") + + click_link "Edit Profile" + fill_in "user_homepage_url", with: "https://nickisawesome.com" + fill_in "Password", with: PasswordHelpers::SECURE_TEST_PASSWORD + click_button "Update" + + click_link "Sign out" + visit profile_path("nick1") + + assert page.has_link?("https://nickisawe...") + end + test "deleting profile" do sign_in visit profile_path("nick1") diff --git a/test/models/user_test.rb b/test/models/user_test.rb index add6d52b9a5..317782c38aa 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -172,6 +172,19 @@ class UserTest < ActiveSupport::TestCase should_not allow_value("012345678901234567890").for(:twitter_username) end + context "homepage url" do + should allow_value("https://www.mywebsite.com").for(:homepage_url) + should allow_value("http://www.mywebsite.com").for(:homepage_url) + should_not allow_value("hi").for(:homepage_url) + should_not allow_value("javascript:alert('hello');").for(:homepage_url) + should_not allow_value("file:///etc/passwd").for(:homepage_url) + should_not allow_value("file://C:/Windows/System32/cmd.exe").for(:homepage_url) + should_not allow_value("data:text/html,").for(:homepage_url) + should_not allow_value("data:text/html;base64,SGVsbG8sIFdvcmxkIQ==").for(:homepage_url) + should_not allow_value("data:text/html,").for(:homepage_url) + should_not allow_value("data:text/html,").for(:homepage_url) + end + context "password" do should "be between 10 characters and 72 bytes" do user = build(:user, password: "%5a&12ed/") diff --git a/test/unit/helpers/url_helper_test.rb b/test/unit/helpers/url_helper_test.rb new file mode 100644 index 00000000000..5b271675323 --- /dev/null +++ b/test/unit/helpers/url_helper_test.rb @@ -0,0 +1,33 @@ +require "test_helper" + +class UrlHelperTest < ActionView::TestCase + include ERB::Util + context "display_safe_url" do + should "return url if it begins with https" do + assert_equal "https://www.awesomesite.com", display_safe_url("https://www.awesomesite.com") + end + should "return empty string if url is empty" do + assert_equal "", display_safe_url("") + end + + should "display a url starting with http" do + assert_equal "http://www.awesomesite.com", display_safe_url("http://www.awesomesite.com") + end + + should "return link with https if it does not begin with https" do + assert_equal "https://javascript:alert('hello');", display_safe_url("javascript:alert('hello');") + end + + should "escape html" do + assert_equal "https://<script>alert('hello');</script>https://www", display_safe_url("https://www") + end + + should "prepend https if url does not begin with http or https" do + assert_equal "https://www.awesomesite.com/https://javascript:alert('hello');", display_safe_url("www.awesomesite.com/https://javascript:alert('hello');") + end + + should "return empty string if url is nil" do + assert_equal "", display_safe_url(nil) + end + end +end