diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e8fd5f6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: ci +on: push +# push: +# tags: +# - '*' +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: RSpec + run: bundle exec rspec spec/ + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + dist/dev-unmanual-${{ github.ref }}.pdf% \ No newline at end of file diff --git a/.gitignore b/.gitignore index 39f02f3..42eff0f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ pkg/* .bundle Gemfile.lock .ruby-gemset -.ruby-version \ No newline at end of file +vendor/ diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..460b6fd --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.7.5 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 14ac5c0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: ruby -rvm: - - 2.1.0 - - 2.0.0 - - 1.9.3 -before_install: - - gem update bundler diff --git a/Changelog.md b/Changelog.md index 9282784..cd94277 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,4 +1,17 @@ +# 0.1.0 + +* Smolbars rename +* Replace therubyracer with mini_racer + * No more invoking Ruby from JS + * We build templates through string concatentation. Do not pass in untrusted handlebars templates. + * All data passed to JS environment is serialized with JSON + * Remove support for partial_missing, it's deprecated since handlebars 4.3.0 + * Remove precompiling (doesn't seem useful in the ruby setting) + * Remove setting data from ruby (use eval and do it yourself) + * Remove support for SafeString (use eval and do it yourself) + # 0.8.0 + * bumped handlebars-source version to 4.0.5 # 0.2.3 diff --git a/Gemfile b/Gemfile index c9e384a..ec5b991 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" -# Specify your gem's dependencies in handlebars.gemspec +# Specify your gem's dependencies in smolbars.gemspec gemspec diff --git a/README.mdown b/README.mdown index 1ac5db5..1491953 100644 --- a/README.mdown +++ b/README.mdown @@ -1,123 +1,87 @@ -## Handlebars.rb +## Smolbars -[](http://badge.fury.io/rb/handlebars) +[](http://badge.fury.io/rb/handlebars) [](https://travis-ci.org/cowboyd/handlebars.rb) [](https://gemnasium.com/cowboyd/handlebars.rb) -This uses [therubyracer][1] to bind to the _actual_ JavaScript implementation of -[Handlebars.js][2] so that you can use it from ruby. +This uses [mini_racer][1] to bind to the _actual_ JavaScript implementation of +[Handlebars.js][2] so that you can use it from ruby. This is a fork of [handlebars.rb][3] to +change out the deprecated [therubyracer][4] JS integration. 99% the same idea as the better-named +[minibars][5]. + +Please be mindful of how this library works: it brings in the full libv8 JS VM to your ruby environment. Each +`Context` is a full blown JS machine (memory management, JIT, etc). This fork does not support attaching ruby +functions to the JS VM. + +Note on security: do not compile untrusted Handlebars templates. We compile Handlebars template by building ad-hoc +javascript statements, a bad actor could perform an SQL-injection like attack using the v8 environment for bad things. ## Usage ### Simple stuff - require 'handlebars' - handlebars = Handlebars::Context.new - template = handlebars.compile("{{say}} {{what}}") + require 'smolbars' + smolbars = Smolbars::Context.new + template = smolbars.compile("{{say}} {{what}}") template.call(:say => "Hey", :what => "Yuh!") #=> "Hey Yuh!" -### functions as properties - - template.call(:say => "Hey ", :what => lambda {|this| ("yo" * 2) + "!"}) #=> "Hey yoyo!" - -### Block Helpers: - -Just like JavaScript, you can write block helpers with an `{{else}}` section. To print -out a section twice if a condition is met: - - # V8 maps the first argument sent to a block to "this". All subsequent arguments are as - # described in the Handlebars documentation. - handlebars.register_helper(:twice) do |context, condition, block| - if condition - "#{block.fn(context)}#{block.fn(context)}" - else - block.inverse(context) - end - end - template = handlebars.compile("{{#twice foo}}Hurray!{{else}}Boo!{{/twice}}") - template.call(foo: true) #=> Hurray!Hurray! - template.call(foo: false) #=> Boo! - -### Private variables: - -Just like JavaScript, block helpers can inject private variables into their child templates. -These can be accessed in a template using the `@` prefix: - - handlebars.register_helper(:list) do |this, context, block| - "
Totally Safe!")})
+ return buffer;
+ });
+ }
+ smolbars = Smolbars::Context.new
+ smolbars.eval(helper)
+ template = smolbars.compile('{{#nthTimes 2}}yep {{/nthTimes}}hurrah!')
+ template.call # 'yep yep hurrah!'
### Partials
-You can directly register partials
-
- handlebars.register_partial("whoami", "I am {{who}}")
- handlebars.compile("{{>whoami}}").call(:who => 'Legend') #=> I am Legend
-
-Partials can also be dynamically looked up by defining a partial_missing behavior:
+You must write partials with JavaScript. The JavaScript code should include calls to the Handlebars class registration
+function.
- handlebars.partial_missing do |name|
- "unable to find >#{name}"
- end
- handlebars.compile("{{>missing}}").call #=> unable to find >missing
-
-Missing partials can also be returned as a function:
-
- count = 0
- handlebars.partial_missing do |name|
- lambda do |this, context, options|
- count += 1
- "#{count} miss(es) when trying to look up a partial"
- end
- end
- t = handlebars.compile("{{>missing}}")
- t.call #=> 1 miss(es) when trying to look up a partial
- t.call #=> 2 miss(es) when tyring to look up a partial
+ require 'smolbars'
+ partial = %Q{
+ Handlebars.registerPartial("legend", "I am {{ who }}");
+ }
+ smolbars = Smolbars::Context.new
+ smolbars.eval(partial)
+ template = smolbars.compile('{{> legend}}')
+ template.call # 'I am Legend!'
### Security
In general, you should not trust user-provided templates: a template can call any method
-(with no arguments) or access any property on any object in the `Handlebars::Context`.
+(with no arguments) or access any property on any object in the `Smolbars::Context`.
If you'd like to render user-provided templates, you'd want to make sure you do so in a
sanitized Context, e.g. no filesystem access, read-only or no database access, etc.
+You can try setting the timeout on a Smolbars::Context through kwargs that are passed to the
+underlying JS instance
+
+ Smolbars::Context.new(timeout: 500)
+
## Test
rspec spec/
-[1]: http://github.com/cowboyd/therubyracer "The Ruby Racer"
-[2]: http://github.com/wycats/handlebars.js "Handlebars JavaScript templating library"
+[1]: https://github.com/rubyjs/mini_racer "mini_racer"
+[2]: https://github.com/wycats/handlebars.js "Handlebars JavaScript templating library"
+[3]: https://github.com/cowboyd/handlebars.rb "Handlebars Ruby library"
+[4]: https://github.com/cowboyd/therubyracer "The Ruby Racer"
+[5]: https://github.com/combinaut/minibars "Minibars"
\ No newline at end of file
diff --git a/handlebars.gemspec b/handlebars.gemspec
index a5db306..3249f74 100644
--- a/handlebars.gemspec
+++ b/handlebars.gemspec
@@ -1,19 +1,19 @@
-require "./lib/handlebars/version"
+require "./lib/smolbars/version"
Gem::Specification.new do |s|
- s.name = "handlebars"
- s.version = Handlebars::VERSION
- s.authors = ["Charles Lowell"]
+ s.name = "smolbars"
+ s.version = Smolbars::VERSION
+ s.authors = ["Charles Lowell", "Xavier Lange"]
s.email = ["cowboyd@thefrontside.net"]
- s.homepage = "https://github.com/cowboyd/handlebars.rb"
- s.summary = "Ruby bindings for the handlebars.js templating library"
- s.description = "Uses the actual JavaScript implementation of Handlebars, but supports using Ruby objects as template contexts and Ruby procs as view functions and named helpers"
+ s.homepage = "https://github.com/cowboyd/smolbars.rb"
+ s.summary = "Ruby bindings for the smolbars.js templating library"
+ s.description = "Uses the actual JavaScript implementation of Handlebars"
s.license = "MIT"
s.files = `git ls-files lib README.mdown`.split("\n")
- s.add_dependency "therubyracer", "~> 0.12.1"
- s.add_dependency "handlebars-source", "~> 4.0.5"
+ s.add_dependency "mini_racer"
+ s.add_dependency "handlebars-source"
s.add_development_dependency "rake"
s.add_development_dependency "rspec", "~> 2.0"
end
diff --git a/lib/handlebars.rb b/lib/handlebars.rb
deleted file mode 100644
index 6f97daf..0000000
--- a/lib/handlebars.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Handlebars
-end
-
-require 'handlebars/context'
-require 'handlebars/template'
-require 'handlebars/partials'
-require 'handlebars/safe_string'
diff --git a/lib/handlebars/context.rb b/lib/handlebars/context.rb
deleted file mode 100644
index 520e5fb..0000000
--- a/lib/handlebars/context.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-require 'handlebars/source'
-require 'v8'
-
-module Handlebars
- class Context
- def initialize
- @js = V8::Context.new
- @js['global'] = {} # there may be a more appropriate object to be used here @MHW
- @js.load(Handlebars::Source.bundled_path)
-
- @partials = handlebars.partials = Handlebars::Partials.new
- end
-
- def compile(*args)
- ::Handlebars::Template.new(self, handlebars.compile(*args))
- end
-
- def load_helpers(helpers_pattern)
- Dir[helpers_pattern].each{ |path| load_helper(path) }
- end
-
- def load_helper(path)
- @js.load(path)
- end
-
- def precompile(*args)
- handlebars.precompile(*args)
- end
-
- def register_helper(name, &fn)
- handlebars.registerHelper(name, fn)
- end
-
- def register_partial(name, content)
- handlebars.registerPartial(name, content)
- end
-
- def create_frame(data)
- handlebars.createFrame(data)
- end
-
- def partial_missing(&fn)
- @partials.partial_missing = fn
- end
-
- def handlebars
- @js.eval('Handlebars')
- end
-
- def []=(key, value)
- data[key] = value
- end
-
- def [](key)
- data[key]
- end
-
- class << self
- attr_accessor :current
- end
-
- private
-
- def data
- handlebars[:_rubydata] ||= handlebars.create()
- end
- end
-end
diff --git a/lib/handlebars/partials.rb b/lib/handlebars/partials.rb
deleted file mode 100644
index 300051d..0000000
--- a/lib/handlebars/partials.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module Handlebars
- class Partials
- attr_accessor :partial_missing
-
- def initialize
- @partials = {}
- end
-
- def []=(name, value)
- @partials[name.to_s] = value
- end
-
- def [](name)
- if @partials.has_key?(name.to_s)
- return @partials[name.to_s]
- elsif @partial_missing
- return @partial_missing[name]
- else
- yield
- end
- end
- end
-end
\ No newline at end of file
diff --git a/lib/handlebars/safe_string.rb b/lib/handlebars/safe_string.rb
deleted file mode 100644
index 32c2f31..0000000
--- a/lib/handlebars/safe_string.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module Handlebars
- class SafeString
- def self.new(string)
- if context = Context.current
- context.handlebars['SafeString'].new(string)
- else
- fail "Cannot instantiate Handlebars.SafeString outside a running template Evaluation"
- end
- end
- end
-end
\ No newline at end of file
diff --git a/lib/handlebars/template.rb b/lib/handlebars/template.rb
deleted file mode 100644
index f68a5dc..0000000
--- a/lib/handlebars/template.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Handlebars
- class Template
- def initialize(context, fn)
- @context, @fn = context, fn
- end
-
- def call(*args)
- current = Handlebars::Context.current
- Handlebars::Context.current = @context
- @fn.call(*args)
- ensure
- Handlebars::Context.current = current
- end
- end
-end
\ No newline at end of file
diff --git a/lib/handlebars/version.rb b/lib/handlebars/version.rb
deleted file mode 100644
index e056f24..0000000
--- a/lib/handlebars/version.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-module Handlebars
- VERSION = "0.8.0"
-end
diff --git a/lib/smolbars.rb b/lib/smolbars.rb
new file mode 100644
index 0000000..e8a9616
--- /dev/null
+++ b/lib/smolbars.rb
@@ -0,0 +1,2 @@
+require 'smolbars/context'
+require 'smolbars/template'
diff --git a/lib/smolbars/context.rb b/lib/smolbars/context.rb
new file mode 100644
index 0000000..2c98dff
--- /dev/null
+++ b/lib/smolbars/context.rb
@@ -0,0 +1,43 @@
+require 'handlebars/source'
+require 'mini_racer'
+require 'securerandom'
+
+module Smolbars
+ class Context
+ def initialize(**kwargs)
+ @@snapshot ||= MiniRacer::Snapshot.new(File.read(Handlebars::Source.bundled_path))
+ @js = MiniRacer::Context.new(kwargs.merge(snapshot: @@snapshot))
+ end
+
+ # Note that this is a hacky JS expression builder. We cannot pass JS AST in to mini_racer so we have to
+ # hope the template passed in does not form invalid Ruby. So don't use templates with backtick characters without
+ # manually escaping them
+ def compile(template)
+ if template.include?("`")
+ raise RuntimeError.new("template cannot contain a backtick character '`'")
+ end
+ handle = fn_handle
+ invocation = %Q{var #{handle} = Handlebars.compile(`#{template}`);}
+ @js.eval(invocation)
+ ::Smolbars::Template.new(self, handle)
+ end
+
+ def eval(*args)
+ @js.eval(*args)
+ end
+
+ def load_pattern(pattern)
+ Dir[pattern].each{ |path| load(path) }
+ end
+
+ def load(path)
+ @js.load(path)
+ end
+
+ private
+
+ def fn_handle
+ "js_fn_#{SecureRandom.hex}"
+ end
+ end
+end
diff --git a/lib/smolbars/template.rb b/lib/smolbars/template.rb
new file mode 100644
index 0000000..f9a9f87
--- /dev/null
+++ b/lib/smolbars/template.rb
@@ -0,0 +1,17 @@
+module Smolbars
+ class Template
+ def initialize(context, fn)
+ @context, @fn = context, fn
+ end
+
+ def call(*args, **kwargs)
+ if args.length == 0
+ invocation = "%s(%s)" % [@fn, kwargs.to_json]
+ else
+ raise "unsupported"
+ invocation = "%s(%s)" % [@fn, args.to_json]
+ end
+ @context.eval(invocation)
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/smolbars/version.rb b/lib/smolbars/version.rb
new file mode 100644
index 0000000..39e40e4
--- /dev/null
+++ b/lib/smolbars/version.rb
@@ -0,0 +1,3 @@
+module Smolbars
+ VERSION = "0.1.0"
+end
diff --git a/spec/handlebars_spec.rb b/spec/handlebars_spec.rb
deleted file mode 100644
index cc84c63..0000000
--- a/spec/handlebars_spec.rb
+++ /dev/null
@@ -1,145 +0,0 @@
-require 'handlebars'
-describe(Handlebars::Context) do
-
- describe "a simple template" do
- let(:t) { compile("Hello {{name}}") }
- it "allows simple subsitution" do
- t.call(:name => 'World').should eql "Hello World"
- end
-
- it "allows Ruby blocks as a property" do
- t.call(:name => lambda { |context| ; "Mate" }).should eql "Hello Mate"
- end
-
- it "can use any Ruby object as a context" do
- t.call(double(:Object, :name => "Flipper")).should eql "Hello Flipper"
- end
- end
-
- describe "allows Handlebars whitespace operator" do
- let(:t) { compile("whitespace {{~word~}} be replaced.") }
- it "consumes all whitespace characters before/after the tag with the whitespace operator" do
- t.call(:word => "should").should eql "whitespaceshouldbe replaced."
- end
- end
-
- describe "loading Helpers" do
- before do
- subject.load_helper('spec/sample_helper.js')
- end
-
- it "can call helpers defined in a javascript file" do
- t = compile('{{#nthTimes 2}}yep {{/nthTimes}}hurrah!')
- t.call.should eql 'yep yep hurrah!'
- end
- end
-
- describe "registering Helpers" do
- before do
- subject.register_helper('alsowith') do |this, context, block|
- block.fn(context)
- end
- subject.register_helper(:twice) do |this, block|
- "#{block.fn}#{block.fn}"
- end
- end
-
- it "correctly passes context and implementation" do
- t = compile("it's so {{#alsowith weather}}*{{summary}}*{{/alsowith}}!")
- t.call(:weather => {:summary => "sunny"}).should eql "it's so *sunny*!"
- end
-
- it "doesn't nee a context or arguments to the call" do
- t = compile("{{#twice}}Hurray!{{/twice}}")
- t.call.should eql "Hurray!Hurray!"
- end
- end
-
- describe "registering Partials" do
- before do
- subject.register_partial('legend', 'I am {{who}}')
- end
- it "renders partials" do
- compile("{{> legend}}").call(:who => 'Legend!').should eql "I am Legend!"
- end
- end
-
- describe "dynamically loading partial" do
- it "can be done with a string" do
- subject.partial_missing do |name|
- "unable to find >#{name}"
- end
- compile("I am {{>missing}}").call().should eql "I am unable to find >missing"
- end
-
- it "can be done with a function" do
- subject.partial_missing do |name|
- lambda do |this, context, options|
- "unable to find my #{name} #{context.what}"
- end
- end
- compile("I am {{>missing}}").call(:what => 'shoes').should eql "I am unable to find my missing shoes"
- end
- end
-
- describe "creating safe strings from ruby" do
- let(:t) { subject.compile("{{safe}}") }
- it "respects safe strings returned from ruby blocks" do
- t.call(:safe => lambda { |this, *args| Handlebars::SafeString.new("totally safe
") }).should eql "totally safe
"
- end
- end
-
- describe "context specific data" do
- before { subject['foo'] = 'bar' }
- it 'can be get and set' do
- subject['foo'].should eql 'bar'
- end
- end
-
- describe "precompiling templates" do
- let(:t) { precompile("foo {{bar}}") }
- it "should compile down to javascript" do
- t.should include 'function'
- end
- end
-
- describe "private variables" do
- let(:t) {subject.compile("{{#list array}}{{@index}}. {{title}} {{@dummy}}{{/list}}")}
- before do
- subject.register_helper('list') do |this, context, block|
- "