Fix Phlex composition and safety regressions

This commit is contained in:
Sami Samhuri 2026-02-07 16:47:00 -08:00
parent 848054f941
commit c8b75cdd83
No known key found for this signature in database
13 changed files with 197 additions and 36 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -44,7 +44,7 @@ module Pressa
div(class: 'row clearfix') do
p(class: 'fin') do
raw(Icons.code)
raw(safe(Icons.code))
end
end

View file

@ -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

View 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

View 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
View 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('&lt;article&gt;')
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: &lt;img src=x onerror=alert(1)&gt;</title>')
end
end