コミットを比較
4 コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| ff2954cc63 | |||
| 3a8f7d8d89 | |||
| 84fe36a405 | |||
| b19c24cb8e |
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする