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