コミットを比較

..

4 コミット

作成者 SHA1 メッセージ 日付
みてるぞ ff2954cc63 #398 2026-07-10 00:56:36 +09:00
みてるぞ 3a8f7d8d89 #398 2026-07-10 00:34:45 +09:00
みてるぞ 84fe36a405 #398 2026-07-09 23:33:10 +09:00
みてるぞ b19c24cb8e #398 2026-07-09 23:11:31 +09:00
9個のファイルの変更8行の追加157行の削除
+4 -22
ファイルの表示
@@ -28,29 +28,11 @@ module Preview
raise FetchFailed, 'redirect が多すぎます.' raise FetchFailed, 'redirect が多すぎます.'
end end
if location.blank? raise FetchFailed, 'redirect 先が不正です.' if location.blank?
log_failure(:blank_redirect_location,
url: uri.to_s,
redirects:,
content_type: response['content-type'],
content_length: response['content-length'])
raise FetchFailed, 'redirect 先が不正です.'
end
redirect_url = return fetch(URI.join(uri, location).to_s,
begin max_bytes:,
URI.join(uri, location).to_s redirects: redirects - 1)
rescue URI::InvalidURIError => e
log_failure(:invalid_redirect_location,
url: uri.to_s,
redirects:,
location:,
error: e.class.name,
message: e.message)
raise FetchFailed, 'redirect 先が不正です.'
end
return fetch(redirect_url, max_bytes:, redirects: redirects - 1)
end end
unless response.is_a?(Net::HTTPSuccess) unless response.is_a?(Net::HTTPSuccess)
+3 -12
ファイルの表示
@@ -1,9 +1,6 @@
module Preview module Preview
class ThumbnailFetcher class ThumbnailFetcher
class GenerationFailed < StandardError; end class GenerationFailed < StandardError; end
ALLOWED_IMAGE_CONTENT_TYPES = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp'
].freeze
HTML_MAX_BYTES = 1.megabyte HTML_MAX_BYTES = 1.megabyte
NICONICO_XML_MAX_BYTES = 256.kilobytes NICONICO_XML_MAX_BYTES = 256.kilobytes
@@ -35,7 +32,7 @@ module Preview
return nil if url.blank? return nil if url.blank?
response = HttpFetcher.fetch(url) response = HttpFetcher.fetch(url)
return nil unless allowed_image_content_type?(response.content_type) return nil unless response.content_type.downcase.start_with?('image/')
response.body response.body
rescue HttpFetcher::FetchTimeout rescue HttpFetcher::FetchTimeout
@@ -46,7 +43,7 @@ module Preview
def self.fetch_image!(url) def self.fetch_image!(url)
response = HttpFetcher.fetch(url) response = HttpFetcher.fetch(url)
unless allowed_image_content_type?(response.content_type) unless response.content_type.downcase.start_with?('image/')
raise GenerationFailed, 'サムネール画像が見つかりませんでした.' raise GenerationFailed, 'サムネール画像が見つかりませんでした.'
end end
@@ -83,13 +80,7 @@ module Preview
nil nil
end end
def self.allowed_image_content_type?(content_type)
mime_type = content_type.to_s.split(';', 2).first.downcase.strip
ALLOWED_IMAGE_CONTENT_TYPES.include?(mime_type)
end
private_class_method :fetch_image_or_nil, :fetch_image!, private_class_method :fetch_image_or_nil, :fetch_image!,
:niconico_thumbnail_url, :niconico_thumbnail_url
:allowed_image_content_type?
end end
end end
-2
ファイルの表示
@@ -40,8 +40,6 @@ module Preview
end end
[uri, parsed_addresses.map(&:to_s)] [uri, parsed_addresses.map(&:to_s)]
rescue Resolv::ResolvError
raise UnsafeUrl, 'URL のホストを解決できません.'
rescue URI::InvalidURIError, IPAddr::InvalidAddressError rescue URI::InvalidURIError, IPAddr::InvalidAddressError
raise UnsafeUrl, 'URL が不正です.' raise UnsafeUrl, 'URL が不正です.'
end end
-23
ファイルの表示
@@ -1,23 +0,0 @@
require 'rails_helper'
RSpec.describe Preview::HttpFetcher do
describe '.fetch' do
it 'raises FetchFailed when redirect location is invalid' do
redirect = Net::HTTPFound.new('1.1', '302', 'Found')
redirect['location'] = 'http://[invalid'
allow(Preview::UrlSafety).to receive(:validate)
.with('https://example.com/page')
.and_return([URI.parse('https://example.com/page'), ['203.0.113.10']])
allow(described_class).to receive(:request)
.and_return(redirect)
allow(Rails.logger).to receive(:warn)
expect {
described_class.fetch('https://example.com/page')
}.to raise_error(Preview::HttpFetcher::FetchFailed)
expect(Rails.logger).to have_received(:warn)
.with(/invalid_redirect_location/)
end
end
end
-51
ファイルの表示
@@ -1,51 +0,0 @@
require 'rails_helper'
RSpec.describe Preview::ThumbnailFetcher do
describe '.fetch' do
it 'rejects svg thumbnails' do
page = Preview::HttpFetcher::Response.new(
'<meta property="og:image" content="https://example.com/thumb.svg">',
'text/html',
'https://example.com/page')
svg = Preview::HttpFetcher::Response.new(
'<svg></svg>',
'image/svg+xml',
'https://example.com/thumb.svg')
allow(Preview::UrlSafety).to receive(:validate)
.and_return([URI.parse('https://example.com/page'), ['203.0.113.10']])
allow(Preview::HttpFetcher).to receive(:fetch)
.with('https://example.com/page', max_bytes: described_class::HTML_MAX_BYTES)
.and_return(page)
allow(Preview::HttpFetcher).to receive(:fetch)
.with('https://example.com/thumb.svg')
.and_return(svg)
expect {
described_class.fetch('https://example.com/page')
}.to raise_error(Preview::ThumbnailFetcher::GenerationFailed)
end
it 'accepts allowed image content type with parameters' do
page = Preview::HttpFetcher::Response.new(
'<meta property="og:image" content="https://example.com/thumb.jpg">',
'text/html',
'https://example.com/page')
image = Preview::HttpFetcher::Response.new(
'jpeg-bytes',
'image/jpeg; charset=binary',
'https://example.com/thumb.jpg')
allow(Preview::UrlSafety).to receive(:validate)
.and_return([URI.parse('https://example.com/page'), ['203.0.113.10']])
allow(Preview::HttpFetcher).to receive(:fetch)
.with('https://example.com/page', max_bytes: described_class::HTML_MAX_BYTES)
.and_return(page)
allow(Preview::HttpFetcher).to receive(:fetch)
.with('https://example.com/thumb.jpg')
.and_return(image)
expect(described_class.fetch('https://example.com/page')).to eq('jpeg-bytes')
end
end
end
-15
ファイルの表示
@@ -1,15 +0,0 @@
require 'rails_helper'
RSpec.describe Preview::UrlSafety do
describe '.validate' do
it 'raises UnsafeUrl when DNS resolution fails' do
allow(Resolv).to receive(:getaddresses)
.with('missing.example')
.and_raise(Resolv::ResolvError)
expect {
described_class.validate('https://missing.example')
}.to raise_error(Preview::UrlSafety::UnsafeUrl)
end
end
end
-5
ファイルの表示
@@ -239,11 +239,6 @@ body
background: var(--top-nav-submenu-bg); background: var(--top-nav-submenu-bg);
} }
.top-nav-mobile-menu .top-nav-submenu
{
background: var(--top-nav-mobile-active-bg);
}
.top-nav-mobile-menu .top-nav-mobile-menu
{ {
background: var(--top-nav-mobile-menu-bg); background: var(--top-nav-mobile-menu-bg);
-22
ファイルの表示
@@ -1,22 +0,0 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
const indexCss = readFileSync (
resolve (process.cwd (), 'src/index.css'),
'utf8')
const compactIndexCss = indexCss.replace (/\s+/g, ' ')
const mobileActiveRule = '.top-nav-mobile-active {'
+ ' background: var(--top-nav-mobile-active-bg); }'
const mobileSubmenuRule = '.top-nav-mobile-menu .top-nav-submenu {'
+ ' background: var(--top-nav-mobile-active-bg); }'
describe ('TopNav mobile menu colours', () => {
it ('uses one background variable for active rows and expanded submenus', () => {
expect (compactIndexCss).toContain (mobileActiveRule)
expect (compactIndexCss).toContain (mobileSubmenuRule)
})
})
+1 -5
ファイルの表示
@@ -89,12 +89,8 @@ describe ('settings', () => {
}) })
it ('derives TopNav colours without mixing mobile active backgrounds', () => { it ('derives TopNav colours without mixing mobile active backgrounds', () => {
const tokens = buildThemeTokens ('light', { }) expect (buildThemeTokens ('light', { }).topNavMobileActiveBackground).toBe (
expect (tokens.topNavMobileActiveBackground).toBe (
LIGHT_THEME_TOKENS.topNavRootBackgroundDesktop) LIGHT_THEME_TOKENS.topNavRootBackgroundDesktop)
expect (tokens.topNavMobileActiveBackground).not.toBe (
tokens.topNavRootBackgroundMobile)
expect (buildThemeTokens ('dark', { }).topNavMobileActiveBackground).toBe ( expect (buildThemeTokens ('dark', { }).topNavMobileActiveBackground).toBe (
DARK_THEME_TOKENS.topNavActiveBackground) DARK_THEME_TOKENS.topNavActiveBackground)
}) })