mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
Fix Phlex composition and safety regressions
This commit is contained in:
parent
848054f941
commit
c8b75cdd83
13 changed files with 197 additions and 36 deletions
|
|
@ -60,7 +60,8 @@ module Pressa
|
|||
permalink = @site.url_for(post.path)
|
||||
|
||||
item = {}
|
||||
item[:url] = post.link_post? ? post.link : permalink
|
||||
item[:url] = permalink
|
||||
item[:external_url] = post.link if post.link_post?
|
||||
item[:tags] = post.tags unless post.tags.empty?
|
||||
item[:content_html] = content_html
|
||||
item[:title] = post.link_post? ? "→ #{post.title}" : post.title
|
||||
|
|
|
|||
|
|
@ -132,12 +132,11 @@ module Pressa
|
|||
page_scripts:,
|
||||
page_styles:,
|
||||
page_description:,
|
||||
page_type:
|
||||
page_type:,
|
||||
content:
|
||||
)
|
||||
|
||||
layout.call do
|
||||
content.call
|
||||
end
|
||||
layout.call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -75,12 +75,11 @@ module Pressa
|
|||
canonical_url:,
|
||||
page_scripts:,
|
||||
page_styles:,
|
||||
page_description:
|
||||
page_description:,
|
||||
content:
|
||||
)
|
||||
|
||||
layout.call do
|
||||
content.call
|
||||
end
|
||||
layout.call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ module Pressa
|
|||
end
|
||||
|
||||
def generate(source_path:, target_path:)
|
||||
validate_paths!(source_path:, target_path:)
|
||||
|
||||
FileUtils.rm_rf(target_path)
|
||||
FileUtils.mkdir_p(target_path)
|
||||
|
||||
|
|
@ -23,6 +25,22 @@ module Pressa
|
|||
|
||||
private
|
||||
|
||||
def validate_paths!(source_path:, target_path:)
|
||||
source_abs = absolute_path(source_path)
|
||||
target_abs = absolute_path(target_path)
|
||||
return unless contains_path?(container: target_abs, path: source_abs)
|
||||
|
||||
raise ArgumentError, 'target_path must not be the same as or contain source_path'
|
||||
end
|
||||
|
||||
def absolute_path(path)
|
||||
File.exist?(path) ? File.realpath(path) : File.expand_path(path)
|
||||
end
|
||||
|
||||
def contains_path?(container:, path:)
|
||||
path == container || path.start_with?("#{container}#{File::SEPARATOR}")
|
||||
end
|
||||
|
||||
def copy_static_files(source_path, target_path)
|
||||
public_dir = File.join(source_path, 'public')
|
||||
return unless Dir.exist?(public_dir)
|
||||
|
|
|
|||
|
|
@ -5,10 +5,6 @@ require_relative '../site'
|
|||
require_relative '../views/layout'
|
||||
require_relative '../views/icons'
|
||||
|
||||
class String
|
||||
include Phlex::SGML::SafeObject
|
||||
end
|
||||
|
||||
module Pressa
|
||||
module Utils
|
||||
class MarkdownRenderer
|
||||
|
|
@ -90,13 +86,11 @@ module Pressa
|
|||
page_subtitle:,
|
||||
canonical_url:,
|
||||
page_description:,
|
||||
page_type:
|
||||
page_type:,
|
||||
content: PageView.new(page_title: page_subtitle, body:)
|
||||
)
|
||||
|
||||
content_view = PageView.new(page_title: page_subtitle, body:)
|
||||
layout.call do
|
||||
content_view.call
|
||||
end
|
||||
layout.call
|
||||
end
|
||||
|
||||
class PageView < Phlex::HTML
|
||||
|
|
@ -108,12 +102,12 @@ module Pressa
|
|||
def view_template
|
||||
article(class: 'container') do
|
||||
h1 { @page_title }
|
||||
raw(@body)
|
||||
raw(safe(@body))
|
||||
end
|
||||
|
||||
div(class: 'row clearfix') do
|
||||
p(class: 'fin') do
|
||||
raw(Views::Icons.code)
|
||||
raw(safe(Views::Icons.code))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module Pressa
|
|||
def view_template
|
||||
div do
|
||||
p(class: 'time') { @post.formatted_date }
|
||||
raw(@post.body)
|
||||
raw(safe(@post.body))
|
||||
p do
|
||||
a(class: 'permalink', href: @site.url_for(@post.path)) { '∞' }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ module Pressa
|
|||
:page_type,
|
||||
:canonical_url,
|
||||
:page_scripts,
|
||||
:page_styles
|
||||
:page_styles,
|
||||
:content
|
||||
|
||||
def initialize(
|
||||
site:,
|
||||
|
|
@ -21,7 +22,8 @@ module Pressa
|
|||
page_description: nil,
|
||||
page_type: 'website',
|
||||
page_scripts: [],
|
||||
page_styles: []
|
||||
page_styles: [],
|
||||
content: nil
|
||||
)
|
||||
@site = site
|
||||
@page_subtitle = page_subtitle
|
||||
|
|
@ -30,13 +32,14 @@ module Pressa
|
|||
@canonical_url = canonical_url
|
||||
@page_scripts = page_scripts
|
||||
@page_styles = page_styles
|
||||
@content = content
|
||||
end
|
||||
|
||||
def format_output?
|
||||
true
|
||||
end
|
||||
|
||||
def view_template(&block)
|
||||
def view_template
|
||||
doctype
|
||||
|
||||
html(lang: 'en') do
|
||||
|
|
@ -92,7 +95,7 @@ module Pressa
|
|||
|
||||
body do
|
||||
render_header
|
||||
instance_exec(&block) if block
|
||||
render(content) if content
|
||||
render_footer
|
||||
render_scripts
|
||||
end
|
||||
|
|
@ -140,17 +143,17 @@ module Pressa
|
|||
ul do
|
||||
li(class: 'mastodon') do
|
||||
a(rel: 'me', 'aria-label': 'Mastodon', href: 'https://techhub.social/@sjs') do
|
||||
raw(Icons.mastodon)
|
||||
raw(safe(Icons.mastodon))
|
||||
end
|
||||
end
|
||||
li(class: 'github') do
|
||||
a('aria-label': 'GitHub', href: 'https://github.com/samsonjs') do
|
||||
raw(Icons.github)
|
||||
raw(safe(Icons.github))
|
||||
end
|
||||
end
|
||||
li(class: 'rss') do
|
||||
a('aria-label': 'RSS', href: site.url_for('/feed.xml')) do
|
||||
raw(Icons.rss)
|
||||
raw(safe(Icons.rss))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
require 'phlex'
|
||||
require_relative 'icons'
|
||||
|
||||
class String
|
||||
include Phlex::SGML::SafeObject
|
||||
end
|
||||
|
||||
module Pressa
|
||||
module Views
|
||||
class PostView < Phlex::HTML
|
||||
|
|
@ -28,12 +24,12 @@ module Pressa
|
|||
a(href: @post.path, class: 'permalink') { '∞' }
|
||||
end
|
||||
|
||||
raw(@post.body)
|
||||
raw(safe(@post.body))
|
||||
end
|
||||
|
||||
div(class: 'row clearfix') do
|
||||
p(class: 'fin') do
|
||||
raw(Icons.code)
|
||||
raw(safe(Icons.code))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ module Pressa
|
|||
|
||||
div(class: 'row clearfix') do
|
||||
p(class: 'fin') do
|
||||
raw(Icons.code)
|
||||
raw(safe(Icons.code))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ module Pressa
|
|||
|
||||
div(class: 'row clearfix') do
|
||||
p(class: 'fin') do
|
||||
raw(Icons.code)
|
||||
raw(safe(Icons.code))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
73
spec/posts/json_feed_spec.rb
Normal file
73
spec/posts/json_feed_spec.rb
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
require 'spec_helper'
|
||||
require 'json'
|
||||
require 'tmpdir'
|
||||
|
||||
RSpec.describe Pressa::Posts::JSONFeedWriter do
|
||||
let(:site) do
|
||||
Pressa::Site.new(
|
||||
author: 'Sami Samhuri',
|
||||
email: 'sami@samhuri.net',
|
||||
title: 'samhuri.net',
|
||||
description: 'blog',
|
||||
url: 'https://samhuri.net',
|
||||
image_url: 'https://samhuri.net/images/me.jpg'
|
||||
)
|
||||
end
|
||||
|
||||
let(:posts_by_year) { double('posts_by_year', recent_posts: [post]) }
|
||||
let(:writer) { described_class.new(site:, posts_by_year:) }
|
||||
|
||||
context 'for link posts' do
|
||||
let(:post) do
|
||||
Pressa::Posts::Post.new(
|
||||
slug: 'github-flow-like-a-pro',
|
||||
title: 'GitHub Flow Like a Pro',
|
||||
author: 'Sami Samhuri',
|
||||
date: DateTime.parse('2015-05-28T07:42:27-07:00'),
|
||||
formatted_date: '28th May, 2015',
|
||||
link: 'http://haacked.com/archive/2014/07/28/github-flow-aliases/',
|
||||
body: '<p>hello</p>',
|
||||
excerpt: 'hello...',
|
||||
path: '/posts/2015/05/github-flow-like-a-pro'
|
||||
)
|
||||
end
|
||||
|
||||
it 'uses permalink as url and keeps external_url for destination links' do
|
||||
Dir.mktmpdir do |dir|
|
||||
writer.write_feed(target_path: dir, limit: 30)
|
||||
feed = JSON.parse(File.read(File.join(dir, 'feed.json')))
|
||||
item = feed.fetch('items').first
|
||||
|
||||
expect(item.fetch('id')).to eq('https://samhuri.net/posts/2015/05/github-flow-like-a-pro')
|
||||
expect(item.fetch('url')).to eq('https://samhuri.net/posts/2015/05/github-flow-like-a-pro')
|
||||
expect(item.fetch('external_url')).to eq('http://haacked.com/archive/2014/07/28/github-flow-aliases/')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for regular posts' do
|
||||
let(:post) do
|
||||
Pressa::Posts::Post.new(
|
||||
slug: 'swift-optional-or',
|
||||
title: 'Swift Optional OR',
|
||||
author: 'Sami Samhuri',
|
||||
date: DateTime.parse('2017-10-01T10:00:00-07:00'),
|
||||
formatted_date: '1st October, 2017',
|
||||
body: '<p>hello</p>',
|
||||
excerpt: 'hello...',
|
||||
path: '/posts/2017/10/swift-optional-or'
|
||||
)
|
||||
end
|
||||
|
||||
it 'omits external_url' do
|
||||
Dir.mktmpdir do |dir|
|
||||
writer.write_feed(target_path: dir, limit: 30)
|
||||
feed = JSON.parse(File.read(File.join(dir, 'feed.json')))
|
||||
item = feed.fetch('items').first
|
||||
|
||||
expect(item.fetch('url')).to eq('https://samhuri.net/posts/2017/10/swift-optional-or')
|
||||
expect(item).not_to have_key('external_url')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
33
spec/site_generator_spec.rb
Normal file
33
spec/site_generator_spec.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
require 'spec_helper'
|
||||
require 'fileutils'
|
||||
require 'tmpdir'
|
||||
|
||||
RSpec.describe Pressa::SiteGenerator do
|
||||
let(:site) do
|
||||
Pressa::Site.new(
|
||||
author: 'Sami Samhuri',
|
||||
email: 'sami@samhuri.net',
|
||||
title: 'samhuri.net',
|
||||
description: 'blog',
|
||||
url: 'https://samhuri.net',
|
||||
plugins: [],
|
||||
renderers: []
|
||||
)
|
||||
end
|
||||
|
||||
it 'rejects a target path that matches the source path' do
|
||||
Dir.mktmpdir do |dir|
|
||||
FileUtils.mkdir_p(File.join(dir, 'public'))
|
||||
source_file = File.join(dir, 'public', 'keep.txt')
|
||||
File.write(source_file, 'safe')
|
||||
|
||||
generator = described_class.new(site:)
|
||||
|
||||
expect {
|
||||
generator.generate(source_path: dir, target_path: dir)
|
||||
}.to raise_error(ArgumentError, /must not be the same as or contain source_path/)
|
||||
|
||||
expect(File.read(source_file)).to eq('safe')
|
||||
end
|
||||
end
|
||||
end
|
||||
45
spec/views/layout_spec.rb
Normal file
45
spec/views/layout_spec.rb
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Pressa::Views::Layout do
|
||||
class TestContentView < Phlex::HTML
|
||||
def view_template
|
||||
article do
|
||||
h1 { 'Hello' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:site) do
|
||||
Pressa::Site.new(
|
||||
author: 'Sami Samhuri',
|
||||
email: 'sami@samhuri.net',
|
||||
title: 'samhuri.net',
|
||||
description: 'blog',
|
||||
url: 'https://samhuri.net'
|
||||
)
|
||||
end
|
||||
|
||||
it 'renders child components as HTML instead of escaped text' do
|
||||
html = described_class.new(
|
||||
site:,
|
||||
canonical_url: 'https://samhuri.net/posts/',
|
||||
content: TestContentView.new
|
||||
).call
|
||||
|
||||
expect(html).to include('<article>')
|
||||
expect(html).to include('<h1>Hello</h1>')
|
||||
expect(html).not_to include('<article>')
|
||||
end
|
||||
|
||||
it 'keeps escaping enabled for untrusted string fields' do
|
||||
subtitle = '<img src=x onerror=alert(1)>'
|
||||
html = described_class.new(
|
||||
site:,
|
||||
canonical_url: 'https://samhuri.net/posts/',
|
||||
page_subtitle: subtitle,
|
||||
content: TestContentView.new
|
||||
).call
|
||||
|
||||
expect(html).to include('<title>samhuri.net: <img src=x onerror=alert(1)></title>')
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue