diff --git a/app/controllers/api/v1/peers/search_controller.rb b/app/controllers/api/v1/peers/search_controller.rb
index 2c0eacdcae..0c503d9bc5 100644
--- a/app/controllers/api/v1/peers/search_controller.rb
+++ b/app/controllers/api/v1/peers/search_controller.rb
@@ -41,5 +41,7 @@ class Api::V1::Peers::SearchController < Api::BaseController
domain = TagManager.instance.normalize_domain(domain)
@domains = Instance.searchable.where(Instance.arel_table[:domain].matches("#{Instance.sanitize_sql_like(domain)}%", false, true)).limit(10).pluck(:domain)
end
+ rescue Addressable::URI::InvalidURIError
+ @domains = []
end
end
diff --git a/app/javascript/core/remote_interaction_helper.ts b/app/javascript/core/remote_interaction_helper.ts
index 53d95b5dbe..4da4d49f6e 100644
--- a/app/javascript/core/remote_interaction_helper.ts
+++ b/app/javascript/core/remote_interaction_helper.ts
@@ -140,7 +140,9 @@ const fromAcct = (acct: string) => {
};
const fetchInteractionURL = (uri_or_domain: string) => {
- if (/^https?:\/\//.test(uri_or_domain)) {
+ if (uri_or_domain === '') {
+ fetchInteractionURLFailure();
+ } else if (/^https?:\/\//.test(uri_or_domain)) {
fromURL(uri_or_domain);
} else if (uri_or_domain.includes('@')) {
fromAcct(uri_or_domain);
diff --git a/app/javascript/mastodon/features/interaction_modal/index.jsx b/app/javascript/mastodon/features/interaction_modal/index.jsx
index 84e4309254..2a9fa0e33e 100644
--- a/app/javascript/mastodon/features/interaction_modal/index.jsx
+++ b/app/javascript/mastodon/features/interaction_modal/index.jsx
@@ -100,8 +100,41 @@ class LoginForm extends React.PureComponent {
this.input = c;
};
+ isValueValid = (value) => {
+ let likelyAcct = false;
+ let url = null;
+
+ if (value.startsWith('/')) {
+ return false;
+ }
+
+ if (value.startsWith('@')) {
+ value = value.slice(1);
+ likelyAcct = true;
+ }
+
+ // The user is in the middle of typing something, do not error out
+ if (value === '') {
+ return true;
+ }
+
+ if (/^https?:\/\//.test(value) && !likelyAcct) {
+ url = value;
+ } else {
+ url = `https://${value}`;
+ }
+
+ try {
+ new URL(url);
+ return true;
+ } catch(_) {
+ return false;
+ }
+ };
+
handleChange = ({ target }) => {
- this.setState(state => ({ value: target.value, isLoading: true, error: false, options: addInputToOptions(target.value, state.networkOptions) }), () => this._loadOptions());
+ const error = !this.isValueValid(target.value);
+ this.setState(state => ({ error, value: target.value, isLoading: true, options: addInputToOptions(target.value, state.networkOptions) }), () => this._loadOptions());
};
handleMessage = (event) => {
@@ -115,11 +148,18 @@ class LoginForm extends React.PureComponent {
this.setState({ isSubmitting: false, error: true });
} else if (event.data?.type === 'fetchInteractionURL-success') {
if (/^https?:\/\//.test(event.data.template)) {
- if (localStorage) {
- localStorage.setItem(PERSISTENCE_KEY, event.data.uri_or_domain);
- }
+ try {
+ const url = new URL(event.data.template.replace('{uri}', encodeURIComponent(resourceUrl)));
- window.location.href = event.data.template.replace('{uri}', encodeURIComponent(resourceUrl));
+ if (localStorage) {
+ localStorage.setItem(PERSISTENCE_KEY, event.data.uri_or_domain);
+ }
+
+ window.location.href = url;
+ } catch (e) {
+ console.error(e);
+ this.setState({ isSubmitting: false, error: true });
+ }
} else {
this.setState({ isSubmitting: false, error: true });
}
@@ -259,7 +299,7 @@ class LoginForm extends React.PureComponent {
spellcheck='false'
/>
-
+
{hasPopOut && (