2026-03-18 11:10:07 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
2026-03-20 12:00:07 -04:00
|
|
|
module BookmarkUrl
|
2026-03-18 11:10:07 -04:00
|
|
|
class ArticlesController < ::ApplicationController
|
|
|
|
|
requires_login
|
|
|
|
|
before_action :ensure_enabled!
|
|
|
|
|
before_action :validate_url!
|
|
|
|
|
|
|
|
|
|
def extract
|
|
|
|
|
result = ArticleExtractor.extract(@url)
|
|
|
|
|
|
|
|
|
|
render json: {
|
|
|
|
|
title: result.title,
|
|
|
|
|
byline: result.byline,
|
|
|
|
|
site_name: result.site_name,
|
|
|
|
|
description: result.description,
|
|
|
|
|
markdown: result.markdown,
|
|
|
|
|
url: result.url,
|
|
|
|
|
}
|
|
|
|
|
rescue => e
|
2026-03-20 12:00:07 -04:00
|
|
|
Rails.logger.warn("[bookmark-url] Extraction failed for #{@url}: #{e.message}")
|
2026-03-18 11:10:07 -04:00
|
|
|
render json: { error: "Could not extract article: #{e.message}" }, status: :unprocessable_entity
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
|
|
def ensure_enabled!
|
2026-03-20 12:00:07 -04:00
|
|
|
raise Discourse::NotFound unless SiteSetting.bookmark_url_enabled
|
2026-03-18 11:10:07 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def validate_url!
|
|
|
|
|
raw = params.require(:url)
|
|
|
|
|
|
|
|
|
|
begin
|
|
|
|
|
uri = URI.parse(raw)
|
|
|
|
|
rescue URI::InvalidURIError
|
|
|
|
|
return render json: { error: "Invalid URL" }, status: :bad_request
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
unless %w[http https].include?(uri.scheme)
|
|
|
|
|
return render json: { error: "Only http/https URLs are supported" }, status: :bad_request
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# SSRF protection — block private/loopback addresses
|
2026-03-20 12:00:07 -04:00
|
|
|
blocked_domains = SiteSetting.bookmark_url_blocked_domains
|
2026-03-18 11:10:07 -04:00
|
|
|
.split(",").map(&:strip).reject(&:empty?)
|
|
|
|
|
|
|
|
|
|
if blocked_domains.any? { |d| uri.host&.include?(d) }
|
|
|
|
|
return render json: { error: "Domain not allowed" }, status: :forbidden
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Optionally enforce an allowlist
|
2026-03-20 12:00:07 -04:00
|
|
|
allowed_domains = SiteSetting.bookmark_url_allowed_domains
|
2026-03-18 11:10:07 -04:00
|
|
|
.split(",").map(&:strip).reject(&:empty?)
|
|
|
|
|
|
|
|
|
|
if allowed_domains.any? && !allowed_domains.any? { |d| uri.host&.end_with?(d) }
|
|
|
|
|
return render json: { error: "Domain not in allowlist" }, status: :forbidden
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@url = raw
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|