mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-04-27 14:57:40 +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)
|
permalink = @site.url_for(post.path)
|
||||||
|
|
||||||
item = {}
|
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[:tags] = post.tags unless post.tags.empty?
|
||||||
item[:content_html] = content_html
|
item[:content_html] = content_html
|
||||||
item[:title] = post.link_post? ? "→ #{post.title}" : post.title
|
item[:title] = post.link_post? ? "→ #{post.title}" : post.title
|
||||||
|
|
|
||||||
|
|
@ -132,12 +132,11 @@ module Pressa
|
||||||
page_scripts:,
|
page_scripts:,
|
||||||
page_styles:,
|
page_styles:,
|
||||||
page_description:,
|
page_description:,
|
||||||
page_type:
|
page_type:,
|
||||||
|
content:
|
||||||
)
|
)
|
||||||
|
|
||||||
layout.call do
|
layout.call
|
||||||
content.call
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -75,12 +75,11 @@ module Pressa
|
||||||
canonical_url:,
|
canonical_url:,
|
||||||
page_scripts:,
|
page_scripts:,
|
||||||
page_styles:,
|
page_styles:,
|
||||||
page_description:
|
page_description:,
|
||||||
|
content:
|
||||||
)
|
)
|
||||||
|
|
||||||
layout.call do
|
layout.call
|
||||||
content.call
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ module Pressa
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate(source_path:, target_path:)
|
def generate(source_path:, target_path:)
|
||||||
|
validate_paths!(source_path:, target_path:)
|
||||||
|
|
||||||
FileUtils.rm_rf(target_path)
|
FileUtils.rm_rf(target_path)
|
||||||
FileUtils.mkdir_p(target_path)
|
FileUtils.mkdir_p(target_path)
|
||||||
|
|
||||||
|
|
@ -23,6 +25,22 @@ module Pressa
|
||||||
|
|
||||||
private
|
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)
|
def copy_static_files(source_path, target_path)
|
||||||
public_dir = File.join(source_path, 'public')
|
public_dir = File.join(source_path, 'public')
|
||||||
return unless Dir.exist?(public_dir)
|
return unless Dir.exist?(public_dir)
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,6 @@ require_relative '../site'
|
||||||
require_relative '../views/layout'
|
require_relative '../views/layout'
|
||||||
require_relative '../views/icons'
|
require_relative '../views/icons'
|
||||||
|
|
||||||
class String
|
|
||||||
include Phlex::SGML::SafeObject
|
|
||||||
end
|
|
||||||
|
|
||||||
module Pressa
|
module Pressa
|
||||||
module Utils
|
module Utils
|
||||||
class MarkdownRenderer
|
class MarkdownRenderer
|
||||||
|
|
@ -90,13 +86,11 @@ module Pressa
|
||||||
page_subtitle:,
|
page_subtitle:,
|
||||||
canonical_url:,
|
canonical_url:,
|
||||||
page_description:,
|
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
|
||||||
layout.call do
|
|
||||||
content_view.call
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
class PageView < Phlex::HTML
|
class PageView < Phlex::HTML
|
||||||
|
|
@ -108,12 +102,12 @@ module Pressa
|
||||||
def view_template
|
def view_template
|
||||||
article(class: 'container') do
|
article(class: 'container') do
|
||||||
h1 { @page_title }
|
h1 { @page_title }
|
||||||
raw(@body)
|
raw(safe(@body))
|
||||||
end
|
end
|
||||||
|
|
||||||
div(class: 'row clearfix') do
|
div(class: 'row clearfix') do
|
||||||
p(class: 'fin') do
|
p(class: 'fin') do
|
||||||
raw(Views::Icons.code)
|
raw(safe(Views::Icons.code))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ module Pressa
|
||||||
def view_template
|
def view_template
|
||||||
div do
|
div do
|
||||||
p(class: 'time') { @post.formatted_date }
|
p(class: 'time') { @post.formatted_date }
|
||||||
raw(@post.body)
|
raw(safe(@post.body))
|
||||||
p do
|
p do
|
||||||
a(class: 'permalink', href: @site.url_for(@post.path)) { '∞' }
|
a(class: 'permalink', href: @site.url_for(@post.path)) { '∞' }
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ module Pressa
|
||||||
:page_type,
|
:page_type,
|
||||||
:canonical_url,
|
:canonical_url,
|
||||||
:page_scripts,
|
:page_scripts,
|
||||||
:page_styles
|
:page_styles,
|
||||||
|
:content
|
||||||
|
|
||||||
def initialize(
|
def initialize(
|
||||||
site:,
|
site:,
|
||||||
|
|
@ -21,7 +22,8 @@ module Pressa
|
||||||
page_description: nil,
|
page_description: nil,
|
||||||
page_type: 'website',
|
page_type: 'website',
|
||||||
page_scripts: [],
|
page_scripts: [],
|
||||||
page_styles: []
|
page_styles: [],
|
||||||
|
content: nil
|
||||||
)
|
)
|
||||||
@site = site
|
@site = site
|
||||||
@page_subtitle = page_subtitle
|
@page_subtitle = page_subtitle
|
||||||
|
|
@ -30,13 +32,14 @@ module Pressa
|
||||||
@canonical_url = canonical_url
|
@canonical_url = canonical_url
|
||||||
@page_scripts = page_scripts
|
@page_scripts = page_scripts
|
||||||
@page_styles = page_styles
|
@page_styles = page_styles
|
||||||
|
@content = content
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_output?
|
def format_output?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def view_template(&block)
|
def view_template
|
||||||
doctype
|
doctype
|
||||||
|
|
||||||
html(lang: 'en') do
|
html(lang: 'en') do
|
||||||
|
|
@ -92,7 +95,7 @@ module Pressa
|
||||||
|
|
||||||
body do
|
body do
|
||||||
render_header
|
render_header
|
||||||
instance_exec(&block) if block
|
render(content) if content
|
||||||
render_footer
|
render_footer
|
||||||
render_scripts
|
render_scripts
|
||||||
end
|
end
|
||||||
|
|
@ -140,17 +143,17 @@ module Pressa
|
||||||
ul do
|
ul do
|
||||||
li(class: 'mastodon') do
|
li(class: 'mastodon') do
|
||||||
a(rel: 'me', 'aria-label': 'Mastodon', href: 'https://techhub.social/@sjs') do
|
a(rel: 'me', 'aria-label': 'Mastodon', href: 'https://techhub.social/@sjs') do
|
||||||
raw(Icons.mastodon)
|
raw(safe(Icons.mastodon))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
li(class: 'github') do
|
li(class: 'github') do
|
||||||
a('aria-label': 'GitHub', href: 'https://github.com/samsonjs') do
|
a('aria-label': 'GitHub', href: 'https://github.com/samsonjs') do
|
||||||
raw(Icons.github)
|
raw(safe(Icons.github))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
li(class: 'rss') do
|
li(class: 'rss') do
|
||||||
a('aria-label': 'RSS', href: site.url_for('/feed.xml')) do
|
a('aria-label': 'RSS', href: site.url_for('/feed.xml')) do
|
||||||
raw(Icons.rss)
|
raw(safe(Icons.rss))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
require 'phlex'
|
require 'phlex'
|
||||||
require_relative 'icons'
|
require_relative 'icons'
|
||||||
|
|
||||||
class String
|
|
||||||
include Phlex::SGML::SafeObject
|
|
||||||
end
|
|
||||||
|
|
||||||
module Pressa
|
module Pressa
|
||||||
module Views
|
module Views
|
||||||
class PostView < Phlex::HTML
|
class PostView < Phlex::HTML
|
||||||
|
|
@ -28,12 +24,12 @@ module Pressa
|
||||||
a(href: @post.path, class: 'permalink') { '∞' }
|
a(href: @post.path, class: 'permalink') { '∞' }
|
||||||
end
|
end
|
||||||
|
|
||||||
raw(@post.body)
|
raw(safe(@post.body))
|
||||||
end
|
end
|
||||||
|
|
||||||
div(class: 'row clearfix') do
|
div(class: 'row clearfix') do
|
||||||
p(class: 'fin') do
|
p(class: 'fin') do
|
||||||
raw(Icons.code)
|
raw(safe(Icons.code))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ module Pressa
|
||||||
|
|
||||||
div(class: 'row clearfix') do
|
div(class: 'row clearfix') do
|
||||||
p(class: 'fin') do
|
p(class: 'fin') do
|
||||||
raw(Icons.code)
|
raw(safe(Icons.code))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ module Pressa
|
||||||
|
|
||||||
div(class: 'row clearfix') do
|
div(class: 'row clearfix') do
|
||||||
p(class: 'fin') do
|
p(class: 'fin') do
|
||||||
raw(Icons.code)
|
raw(safe(Icons.code))
|
||||||
end
|
end
|
||||||
end
|
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