Compare commits

...

35 Commits

Author SHA1 Message Date
Jakob Linskeseder d069c1caf1 Fix Ruby component publishing documentation
Docker images has to use root because it tries
to write to `/.local` which would fail otherwise.
2026-03-26 22:26:07 +01:00
Jakob Linskeseder 6714e07a6a Fix Composer component definition
This aligns the Composer component with
https://github.com/components/handlebars.js/blob/master/composer.json
2026-03-26 22:15:07 +01:00
handlebars-lang dce542c9a6 v4.7.9 2026-03-26 20:44:05 +00:00
handlebars-lang 8a41389ba5 Update release notes 2026-03-26 20:43:42 +00:00
Jakob Linskeseder 68d8df5a88 Fix security issues
Fixes GHSA-2w6w-674q-4c4q, GHSA-xhpv-hc6g-r9c6, GHSA-3mfm-83xf-c92r, GHSA-2qvq-rjwj-gvw9, GHSA-9cx6-37pm-9jff, GHSA-7rx3-28cr-v5wh, GHSA-442j-39wm-28r2, GHSA-xjpj-3mr7-gcpf
2026-03-26 21:29:35 +01:00
Jakob Linskeseder b2a083136b Fix browser tests
Pinned the browser tests to the latest Playwright version that still works with Node 16.
We need Node 16 for a successful build.
2026-03-25 22:08:13 +01:00
Jakob Linskeseder 9f98c16298 Fix release script
We have to pin Grunt to 1.5.3, since this
is the latest version to support Node 10,
which is needed for the release process.

Also improved the Dockerfile and added
a warning related to "yo release".
2026-03-25 21:44:59 +01:00
Mohamed Akram 45443b4290 Revert "Improve partial indenting performance"
This reverts commit 08fddee033.

The change caused performance regressions in some cases and performance
was not consistent across engines.
2026-03-25 20:31:53 +01:00
Jakob Linskeseder 8841a5f6d3 Fix CI errors with linting 2026-02-07 22:22:17 +01:00
Tyler Blackham e0137c26f2 fix: enable shell mode for spawn to resolve Windows EINVAL issue 2026-02-07 22:12:25 +01:00
Mohamed Akram e914d6037f Improve rendering performance
Avoid unnecessary copies via Utils.extend in hot paths.
2024-09-03 22:08:38 +02:00
Christian Clauss 7de4b41c34 Upgrade GitHub Actions checkout and setup-node on 4.x branch
Like #2024 but on another branch.
2023-12-14 00:07:16 +01:00
Benoit Vallée eab1d141cb Fix type "RuntimeOptions" also accepting string partials 2023-09-21 22:30:08 +02:00
Jonas Thelemann de4414d7fc feat(types): set hash to be a Record<string, any>
Resolves #2001
2023-09-12 23:23:59 +02:00
Mohamed Akram 08fddee033 Improve partial indenting performance 2023-09-07 22:42:30 +02:00
Mohamed Akram 4512766919 Fix non-contiguous program indices 2023-09-07 22:24:57 +02:00
Nils Knappmeier e497a35d7f refactor: rename i to startPartIndex 2023-08-05 19:47:17 +02:00
Nils Knappmeier 8c9f866655 update package-lock.json to correct version 2023-08-05 00:14:57 +02:00
Jakob Linskeseder 520e1d5f08 Fix links in README
This commit backports changes from `master`.
2023-08-02 23:37:05 +02:00
Jakob Linskeseder 02423780a9 Fix link to homepage
When visiting the www-variant, a `SSL_ERROR_INTERNAL_ERROR_ALERT`
error is displayed by the browser.
2023-08-02 23:35:42 +02:00
Jakob Linskeseder be92d2f254 Backport: publish latest and tags to AWS S3
In order to automate publishing 4.x releases to AWS S3,
we also need this workflow on the 4.x branch.

See #1972
2023-08-02 23:09:34 +02:00
Jakob Linskeseder 443a613b3a Add guideline for pushing to RubyGems 2023-08-02 22:56:15 +02:00
Nils Knappmeier 83ee5908f2 add links to publish requirements 2023-08-01 23:24:13 +02:00
Jakob Linskeseder 8dc3d2517b v4.7.8 2023-08-01 23:06:18 +02:00
Jakob Linskeseder 668c4fb878 Fix browser tests in CI pipeline 2023-07-23 20:46:46 +02:00
Jakob Linskeseder c65c6cce3f Test on Node 18
* Replaced outdated `grunt-bg-shell`-package to get rid of
  coffee-script warnings

Backports parts of 785a63e0a8.
2022-10-21 16:48:14 +02:00
Marnix Kok 3d3796c1e9 Make library compatible with workers
When using Handlebars in a Cloudflare Worker, an environment
in which the `window` and `global` objects both don't exist,
an error is thrown about `window` being undefined.

According to the ECMA specification, only `self` is available
in a worker. Since we support old runtimes in our 4.x branch,
we can't use `globalThis` but have to use a polyfill.
2022-10-16 22:17:50 +02:00
Jakob Linskeseder 075b354a3b Fix sync issue with npm lock-file
```
npm ERR! `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm ERR!
npm ERR! Invalid: lock file's acorn@5.7.4 does not satisfy acorn@7.4.1
npm ERR! Missing: acorn@5.7.4 from lock file
```
2022-10-16 21:55:05 +02:00
arturmakaranka 30dbf04781 Fix compiling of each block params in strict mode
Fixes #1820
Closes #1821
2022-05-25 22:18:36 +02:00
Jakob Linskeseder e3a54485db Fix bundler issue with webpack 5
As explained in issue #1844 and in issue
https://github.com/webpack/webpack/issues/15007#issuecomment-996615250,
the way we used the `browser`-field was wrong.

The main reason for using the `browser`-field is the requirement of
`require('fs')` in the main-entry-file.
The workaround for this was using `require('handlebars/lib/handlebars')`,
but now it will also work via `require('handlebars')` for bundlers that
respect the `browser`-field.

The `"./runtime"`-config was removed, because it didn't have any effect.
In order to detect regressions, the webpack-integration test was
extended to test with different webpack versions.

Fixes #1174
Closes #1844
2022-05-17 21:19:48 +02:00
Jakob Linskeseder 8e23642ea2 Fix integration-tests issue with npm >= 7
Fixes the following error when running integration-tests:
```
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: webpack-test@1.0.0
npm ERR! Found: handlebars@5.0.0-alpha.1
npm ERR! node_modules/handlebars
npm ERR!   dev handlebars@"file:../../.." from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer handlebars@">= 1.3.0 < 5" from handlebars-loader@1.7.1
npm ERR! node_modules/handlebars-loader
npm ERR!   dev handlebars-loader@"^1.7.1" from the root project
npm ERR!
```
2022-05-17 21:19:48 +02:00
James Wilson 88ac06875f use https instead of git for mustache submodule 2022-03-19 19:19:27 +01:00
Jakob Linskeseder c68bc08a0d Fix typo 2022-01-08 00:39:29 +01:00
Nils Knappmeier 6cfbc2653a Use eslint-env "shared-node-browser" for shared handlebars code 2022-01-07 18:15:48 +01:00
Jakob Linskeseder b65135acef Replace shields.io with faster badgen.net
Badgen is a faster successor of shields.io:
https://github.com/badgen/badgen.net

Backport from `master`-branch.
2021-12-30 00:58:48 +01:00
53 changed files with 6744 additions and 14085 deletions
+9 -9
View File
@@ -12,10 +12,10 @@ jobs:
runs-on: 'ubuntu-latest'
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '16'
@@ -33,16 +33,16 @@ jobs:
matrix:
operating-system: ['ubuntu-latest', 'windows-latest']
# https://nodejs.org/en/about/releases/
node-version: ['10', '12', '14', '16', '17']
node-version: ['16', '18', '20', '22']
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
@@ -54,7 +54,7 @@ jobs:
- name: Test (Integration)
# https://github.com/webpack/webpack/issues/14532
if: ${{ matrix.node-version != '17' }}
if: ${{ matrix.node-version == '16' }}
run: |
cd ./tests/integration/rollup-test && ./test.sh && cd -
cd ./tests/integration/webpack-babel-test && ./test.sh && cd -
@@ -62,15 +62,15 @@ jobs:
browser:
name: Test (Browser)
runs-on: 'ubuntu-latest'
runs-on: 'ubuntu-22.04'
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '16'
+38
View File
@@ -0,0 +1,38 @@
name: Release
on:
workflow_dispatch:
push:
branches:
- master
tags:
- '*'
jobs:
publish-aws-s3:
name: Publish to AWS S3
runs-on: 'ubuntu-latest'
environment: 'builds.handlebarsjs.com.s3.amazonaws.com'
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Publish
run: |
git config --global user.email "release@handlebarsjs.com"
git config --global user.name "handlebars-lang"
npm run publish:aws
env:
S3_BUCKET_NAME: "builds.handlebarsjs.com"
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
+1
View File
@@ -15,5 +15,6 @@ node_modules
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/test-results/
/tests/integration/*/dist/
/spec/tmp/*
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "spec/mustache"]
path = spec/mustache
url = git://github.com/mustache/spec.git
url = https://github.com/mustache/spec.git
+87 -16
View File
@@ -49,7 +49,7 @@ The `grunt dev` implements watching for tests and allows for in browser testing
If you notice any problems, please report them to the GitHub issue tracker at
[http://github.com/handlebars-lang/handlebars.js/issues](http://github.com/handlebars-lang/handlebars.js/issues).
##Running Tests
## Running Tests
To run tests locally, first install all dependencies.
@@ -86,32 +86,103 @@ You can use the following scripts to make sure that the CI job does not fail:
- **npm run lint** will run `eslint` and fail on warnings
- **npm run format** will run `prettier` on all files
- **npm run check-before-pull-request** will perform all most checks that our CI job does in its build-job, excluding the "integration-test".
- **npm run integration-test** will run integration tests (using old NodeJS versions and integrations with webpack, babel and so on)
- **npm run test:integration** will run integration tests (using old NodeJS versions and integrations with webpack, babel and so on)
These tests only work on a Linux-machine with `nvm` installed (for running tests in multiple versions of NodeJS).
## Releasing the latest version
Before attempting the release Handlebars, please make sure that you have the following authorizations:
- Push-access to `handlebars-lang/handlebars.js`
- Publishing rights on npmjs.com for the `handlebars` package
- Publishing rights on gemfury for the `handlebars-source` package
- Push-access to the repo for legacy package managers: `components/handlebars`
- Push-access to the production-repo of the handlebars site: `handlebars-lang/handlebarsjs.com-github-pages`
- Push-access to [handlebars-lang/handlebars.js](https://github.com/handlebars-lang/handlebars.js/)
- Publishing rights on npmjs.com for the [handlebars](https://www.npmjs.com/package/handlebars) package
- Publishing rights on rubygems for the [handlebars-source](https://rubygems.org/gems/handlebars-source) package
- Push-access to the repo for legacy package managers: [components/handlebars.js](https://github.com/components/handlebars.js)
- Push-access to the production-repo of the handlebars site: [handlebars-lang/docs](https://github.com/handlebars-lang/docs)
_When releasing a previous version of Handlebars, please look into the CONTRIBUNG.md in the corresponding branch._
A full release may be completed with the following:
A full release via Docker may be completed with the following:
```
npm ci
npx grunt
npm publish
1. Create a `Dockerfile` in this folder for releasing
```Dockerfile
FROM node:10-slim
ENV EDITOR=vim
# Update stretch repositories
RUN sed -i -e 's/deb.debian.org/archive.debian.org/g' \
-e 's|security.debian.org|archive.debian.org/|g' \
-e '/stretch-updates/d' /etc/apt/sources.list
# Install release dependencies
RUN apt-get update
RUN apt-get install -y git vim
# Work around deprecated npm dependency install via unauthenticated git-protocol:
# https://github.com/kpdecker/generator-release/blob/87aab9b84c9f083635c3fcc822f18acce1f48736/package.json#L31
RUN git config --system url."https://github.com/".insteadOf git://github.com/
# Configure git
RUN git config --system user.email "release@handlebarsjs.com"
RUN git config --system user.name "handlebars-lang"
RUN mkdir /home/node/.config
RUN mkdir /home/node/.ssh
RUN mkdir /home/node/tmp
# Generate config for yo generator-release:
# https://github.com/kpdecker/generator-release#example
# You have to add a valid GitHub access token! (Used for reading issues and pull requests.)
RUN echo "module.exports = {\n auth: 'oauth',\n token: 'GitHub personal access token'\n};" > /home/node/.config/generator-release
RUN chown -R node:node /home/node/.config
RUN chown -R node:node /home/node/.ssh
RUN chown -R node:node /home/node/tmp
# Add the generated key to GitHub: https://github.com/settings/keys
RUN ssh-keygen -q -t ed25519 -N '' -f /home/node/.ssh/id_ed25519 -C "release@handlebarsjs.com"
RUN chmod 0600 /home/node/.ssh/id_ed25519*
RUN chown node:node /home/node/.ssh/id_ed25519*
```
2. Build and run the Docker image
```bash
docker build --tag handlebars:release .
docker run --rm --interactive --tty \
--volume $PWD:/app \
--workdir /app \
--user $(id -u):$(id -g) \
--env NPM_CONFIG_PREFIX=/home/node/.npm-global \
handlebars:release bash -c 'export PATH=$PATH:/home/node/.npm-global/bin; bash'
```
* Add SSH key to GitHub: `cat /home/node/.ssh/id_ed25519.pub` (https://github.com/settings/keys)
* Add GitHub API token: `vi /home/node/.config/generator-release`
* Execute the following steps:
```bash
npm install
npm install -g yo@1 grunt@1 generator-release
npm run release
# Warning! This step will collect data from GitHub, bump the version,
# create a new commit, create a new tag and push it to GitHub.
# https://github.com/kpdecker/generator-release?tab=readme-ov-file#usage
yo release
npm login
npm publish
yo release:publish components handlebars.js dist/components/
```
6. Publish Ruby `handlebars-source` gem:
```bash
docker run --rm --interactive --tty \
--volume $PWD:/app \
--workdir /app \
ruby:3.2-slim bash
```
* Execute the following steps:
```bash
cd dist/components/
gem build handlebars-source.gemspec
gem push handlebars-source-*.gem
```
cd dist/components/
gem build handlebars-source.gemspec
gem push handlebars-source-*.gem
```
### After the release
After the release, you should check that all places have really been updated. Especially verify that the `latest`-tags
in those places still point to the latest version
+5 -7
View File
@@ -166,11 +166,9 @@ module.exports = function(grunt) {
}
},
bgShell: {
shell: {
integrationTests: {
cmd: './tests/integration/run-integration-tests.sh',
bg: false,
fail: true
command: './tests/integration/run-integration-tests.sh'
}
},
@@ -195,7 +193,7 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-babel');
grunt.loadNpmTasks('grunt-bg-shell');
grunt.loadNpmTasks('grunt-shell');
grunt.loadNpmTasks('grunt-webpack');
grunt.task.loadTasks('tasks');
@@ -214,7 +212,7 @@ module.exports = function(grunt) {
// Requires secret properties from .travis.yaml
grunt.registerTask('extensive-tests-and-publish-to-aws', [
'default',
'bgShell:integrationTests',
'shell:integrationTests',
'metrics',
'publish-to-aws'
]);
@@ -234,6 +232,6 @@ module.exports = function(grunt) {
);
grunt.registerTask('integration-tests', [
'default',
'bgShell:integrationTests'
'shell:integrationTests'
]);
};
+7 -7
View File
@@ -1,18 +1,18 @@
[![CI Build Status](https://img.shields.io/github/workflow/status/handlebars-lang/handlebars.js/ci/master)](https://github.com/handlebars-lang/handlebars.js/actions)
[![CI Build Status](https://github.com/handlebars-lang/handlebars.js/actions/workflows/ci.yml/badge.svg)](https://github.com/handlebars-lang/handlebars.js/actions/workflows/ci.yml)
[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/handlebars/badge?style=rounded)](https://www.jsdelivr.com/package/npm/handlebars)
[![Bundle size](https://img.shields.io/bundlephobia/minzip/handlebars?label=minified%20%2B%20gzipped)](https://bundlephobia.com/package/handlebars)
[![npm downloads](https://badgen.net/npm/dm/handlebars)](https://www.npmjs.com/package/handlebars)
[![npm version](https://badgen.net/npm/v/handlebars)](https://www.npmjs.com/package/handlebars)
[![Bundle size](https://badgen.net/bundlephobia/minzip/handlebars?label=minified%20%2B%20gzipped)](https://bundlephobia.com/package/handlebars)
[![Install size](https://packagephobia.com/badge?p=handlebars)](https://packagephobia.com/result?p=handlebars)
Handlebars.js
=============
Handlebars.js is an extension to the [Mustache templating
language](https://mustache.github.io/) created by Chris Wanstrath.
Handlebars.js and Mustache are both logicless templating languages that
keep the view and the code separated like we all know they should be.
Handlebars provides the power necessary to let you build **semantic templates** effectively with no frustration.
Handlebars is largely compatible with Mustache templates. In most cases it is possible to swap out Mustache with Handlebars and continue using your current templates.
Checkout the official Handlebars docs site at
[https://handlebarsjs.com/](https://handlebarsjs.com) and the live demo at [http://tryhandlebarsjs.com/](http://tryhandlebarsjs.com/).
[handlebarsjs.com](https://handlebarsjs.com) and try our [live demo](https://handlebarsjs.com/playground.html).
Installing
----------
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "4.7.7",
"version": "4.7.9",
"main": "handlebars.js",
"license": "MIT",
"dependencies": {}
+2 -6
View File
@@ -1,7 +1,7 @@
{
"name": "components/handlebars.js",
"description": "Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.",
"homepage": "http://handlebarsjs.com",
"homepage": "https://handlebarsjs.com",
"license": "MIT",
"type": "component",
"keywords": [
@@ -11,13 +11,9 @@
],
"authors": [
{
"name": "Chris Wanstrath",
"homepage": "http://chriswanstrath.com"
"name": "Chris Wanstrath"
}
],
"require": {
"robloach/component-installer": "*"
},
"extra": {
"component": {
"name": "handlebars",
+1 -1
View File
@@ -2,7 +2,7 @@
<package>
<metadata>
<id>handlebars.js</id>
<version>4.7.7</version>
<version>4.7.9</version>
<authors>handlebars.js Authors</authors>
<licenseUrl>https://github.com/handlebars-lang/handlebars.js/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/handlebars-lang/handlebars.js/</projectUrl>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "4.7.7",
"version": "4.7.9",
"license": "MIT",
"jspm": {
"main": "handlebars",
+4 -2
View File
@@ -1,6 +1,8 @@
module.exports = {
env: {
// Handlebars should run natively in the browser
node: false
// Handlebars should not use node or browser-specific APIs
'shared-node-browser': true,
node: false,
browser: false
}
};
+1 -1
View File
@@ -5,7 +5,7 @@ import { registerDefaultDecorators } from './decorators';
import logger from './logger';
import { resetLoggedProperties } from './internal/proto-access';
export const VERSION = '4.7.7';
export const VERSION = '4.7.9';
export const COMPILER_REVISION = 8;
export const LAST_COMPATIBLE_COMPILER_REVISION = 7;
+67
View File
@@ -1,6 +1,7 @@
import parser from './parser';
import WhitespaceControl from './whitespace-control';
import * as Helpers from './helpers';
import Exception from '../exception';
import { extend } from '../utils';
export { parser };
@@ -11,6 +12,9 @@ extend(yy, Helpers);
export function parseWithoutProcessing(input, options) {
// Just return if an already-compiled AST was passed in.
if (input.type === 'Program') {
// When a pre-parsed AST is passed in, validate all node values to prevent
// code injection via type-confused literals.
validateInputAst(input);
return input;
}
@@ -32,3 +36,66 @@ export function parse(input, options) {
return strip.accept(ast);
}
function validateInputAst(ast) {
validateAstNode(ast);
}
function validateAstNode(node) {
if (node == null) {
return;
}
if (Array.isArray(node)) {
node.forEach(validateAstNode);
return;
}
if (typeof node !== 'object') {
return;
}
if (node.type === 'PathExpression') {
if (!isValidDepth(node.depth)) {
throw new Exception(
'Invalid AST: PathExpression.depth must be an integer'
);
}
if (!Array.isArray(node.parts)) {
throw new Exception('Invalid AST: PathExpression.parts must be an array');
}
for (let i = 0; i < node.parts.length; i++) {
if (typeof node.parts[i] !== 'string') {
throw new Exception(
'Invalid AST: PathExpression.parts must only contain strings'
);
}
}
} else if (node.type === 'NumberLiteral') {
if (typeof node.value !== 'number' || !isFinite(node.value)) {
throw new Exception('Invalid AST: NumberLiteral.value must be a number');
}
} else if (node.type === 'BooleanLiteral') {
if (typeof node.value !== 'boolean') {
throw new Exception(
'Invalid AST: BooleanLiteral.value must be a boolean'
);
}
}
Object.keys(node).forEach(propertyName => {
if (propertyName === 'loc') {
return;
}
validateAstNode(node[propertyName]);
});
}
function isValidDepth(depth) {
return (
typeof depth === 'number' &&
isFinite(depth) &&
Math.floor(depth) === depth &&
depth >= 0
);
}
+2 -2
View File
@@ -1,4 +1,4 @@
/* global define */
/* global define, require */
import { isArray } from '../utils';
let SourceNode;
@@ -8,7 +8,7 @@ try {
if (typeof define !== 'function' || !define.amd) {
// We don't support this in AMD environments. For these environments, we assume that
// they are running on the browser and thus have no need for the source-map library.
let SourceMap = require('source-map'); // eslint-disable-line no-undef
let SourceMap = require('source-map');
SourceNode = SourceMap.SourceNode;
}
} catch (err) {
+30 -16
View File
@@ -165,12 +165,10 @@ JavaScriptCompiler.prototype = {
let { programs, decorators } = this.context;
for (i = 0, l = programs.length; i < l; i++) {
if (programs[i]) {
ret[i] = programs[i];
if (decorators[i]) {
ret[i + '_d'] = decorators[i];
ret.useDecorators = true;
}
ret[i] = programs[i];
if (decorators[i]) {
ret[i + '_d'] = decorators[i];
ret.useDecorators = true;
}
}
@@ -535,14 +533,22 @@ JavaScriptCompiler.prototype = {
this.resolvePath('data', parts, 0, true, strict);
},
resolvePath: function(type, parts, i, falsy, strict) {
resolvePath: function(type, parts, startPartIndex, falsy, strict) {
if (this.options.strict || this.options.assumeObjects) {
this.push(strictLookup(this.options.strict && strict, this, parts, type));
this.push(
strictLookup(
this.options.strict && strict,
this,
parts,
startPartIndex,
type
)
);
return;
}
let len = parts.length;
for (; i < len; i++) {
for (let i = startPartIndex; i < len; i++) {
/* eslint-disable no-loop-func */
this.replaceStack(current => {
let lookup = this.nameLookup(current, parts[i], type);
@@ -680,9 +686,18 @@ JavaScriptCompiler.prototype = {
let foundDecorator = this.nameLookup('decorators', name, 'decorator'),
options = this.setupHelperArgs(name, paramSize);
// Store the resolved decorator in a variable and verify it is a function before
// calling it. Without this, unregistered decorators can cause an unhandled TypeError
// (calling undefined), which crashes the process — enabling Denial of Service.
this.decorators.push(['var decorator = ', foundDecorator, ';']);
this.decorators.push([
'if (typeof decorator !== "function") { throw new Error(',
this.quotedString('Missing decorator: "' + name + '"'),
'); }'
]);
this.decorators.push([
'fn = ',
this.decorators.functionCall(foundDecorator, '', [
this.decorators.functionCall('decorator', '', [
'fn',
'props',
'container',
@@ -906,8 +921,8 @@ JavaScriptCompiler.prototype = {
let existing = this.matchExistingProgram(child);
if (existing == null) {
this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children
let index = this.context.programs.length;
// Placeholder to prevent name conflicts for nested children
let index = this.context.programs.push('') - 1;
child.index = index;
child.name = 'program' + index;
this.context.programs[index] = compiler.compile(
@@ -1261,15 +1276,14 @@ JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
);
};
function strictLookup(requireTerminal, compiler, parts, type) {
function strictLookup(requireTerminal, compiler, parts, startPartIndex, type) {
let stack = compiler.popStack(),
i = 0,
len = parts.length;
if (requireTerminal) {
len--;
}
for (; i < len; i++) {
for (let i = startPartIndex; i < len; i++) {
stack = compiler.nameLookup(stack, parts[i], type);
}
@@ -1279,7 +1293,7 @@ function strictLookup(requireTerminal, compiler, parts, type) {
'(',
stack,
', ',
compiler.quotedString(parts[i]),
compiler.quotedString(parts[len]),
', ',
JSON.stringify(compiler.source.currentLocation),
' )'
+2 -1
View File
@@ -20,7 +20,8 @@ export function moveHelperToHooks(instance, helperName, keepHelper) {
if (instance.helpers[helperName]) {
instance.hooks[helperName] = instance.helpers[helperName];
if (!keepHelper) {
delete instance.helpers[helperName];
// Using delete is slow
instance.helpers[helperName] = undefined;
}
}
}
@@ -1,11 +0,0 @@
import { extend } from '../utils';
/**
* Create a new object with "null"-prototype to avoid truthy results on prototype properties.
* The resulting object can be used with "object[property]" to check if a property exists
* @param {...object} sources a varargs parameter of source objects that will be merged
* @returns {object}
*/
export function createNewLookupObject(...sources) {
return extend(Object.create(null), ...sources);
}
+16 -17
View File
@@ -1,32 +1,31 @@
import { createNewLookupObject } from './create-new-lookup-object';
import { extend } from '../utils';
import logger from '../logger';
const loggedProperties = Object.create(null);
export function createProtoAccessControl(runtimeOptions) {
let defaultMethodWhiteList = Object.create(null);
defaultMethodWhiteList['constructor'] = false;
defaultMethodWhiteList['__defineGetter__'] = false;
defaultMethodWhiteList['__defineSetter__'] = false;
defaultMethodWhiteList['__lookupGetter__'] = false;
let defaultPropertyWhiteList = Object.create(null);
// Create an object with "null"-prototype to avoid truthy results on
// prototype properties.
const propertyWhiteList = Object.create(null);
// eslint-disable-next-line no-proto
defaultPropertyWhiteList['__proto__'] = false;
propertyWhiteList['__proto__'] = false;
extend(propertyWhiteList, runtimeOptions.allowedProtoProperties);
const methodWhiteList = Object.create(null);
methodWhiteList['constructor'] = false;
methodWhiteList['__defineGetter__'] = false;
methodWhiteList['__defineSetter__'] = false;
methodWhiteList['__lookupGetter__'] = false;
methodWhiteList['__lookupSetter__'] = false;
extend(methodWhiteList, runtimeOptions.allowedProtoMethods);
return {
properties: {
whitelist: createNewLookupObject(
defaultPropertyWhiteList,
runtimeOptions.allowedProtoProperties
),
whitelist: propertyWhiteList,
defaultValue: runtimeOptions.allowProtoPropertiesByDefault
},
methods: {
whitelist: createNewLookupObject(
defaultMethodWhiteList,
runtimeOptions.allowedProtoMethods
),
whitelist: methodWhiteList,
defaultValue: runtimeOptions.allowProtoMethodsByDefault
}
};
+15 -4
View File
@@ -1,11 +1,22 @@
/* global globalThis */
export default function(Handlebars) {
/* istanbul ignore next */
let root = typeof global !== 'undefined' ? global : window, // eslint-disable-line no-undef
$Handlebars = root.Handlebars;
// https://mathiasbynens.be/notes/globalthis
(function() {
if (typeof globalThis === 'object') return;
Object.prototype.__defineGetter__('__magic__', function() {
return this;
});
__magic__.globalThis = __magic__; // eslint-disable-line no-undef
delete Object.prototype.__magic__;
})();
const $Handlebars = globalThis.Handlebars;
/* istanbul ignore next */
Handlebars.noConflict = function() {
if (root.Handlebars === Handlebars) {
root.Handlebars = $Handlebars;
if (globalThis.Handlebars === Handlebars) {
globalThis.Handlebars = $Handlebars;
}
return Handlebars;
};
+24 -22
View File
@@ -74,17 +74,10 @@ export function template(templateSpec, env) {
}
partial = env.VM.resolvePartial.call(this, partial, context, options);
let extendedOptions = Utils.extend({}, options, {
hooks: this.hooks,
protoAccessControl: this.protoAccessControl
});
options.hooks = this.hooks;
options.protoAccessControl = this.protoAccessControl;
let result = env.VM.invokePartial.call(
this,
partial,
context,
extendedOptions
);
let result = env.VM.invokePartial.call(this, partial, context, options);
if (result == null && env.compile) {
options.partials[options.name] = env.compile(
@@ -92,7 +85,7 @@ export function template(templateSpec, env) {
templateSpec.compilerOptions,
env
);
result = options.partials[options.name](context, extendedOptions);
result = options.partials[options.name](context, options);
}
if (result != null) {
if (options.indent) {
@@ -145,7 +138,7 @@ export function template(templateSpec, env) {
for (let i = 0; i < len; i++) {
let result = depths[i] && container.lookupProperty(depths[i], name);
if (result != null) {
return depths[i][name];
return result;
}
}
},
@@ -254,8 +247,9 @@ export function template(templateSpec, env) {
ret._setup = function(options) {
if (!options.partial) {
let mergedHelpers = Utils.extend({}, env.helpers, options.helpers);
wrapHelpersToPassLookupProperty(mergedHelpers, container);
let mergedHelpers = {};
addHelpers(mergedHelpers, env.helpers, container);
addHelpers(mergedHelpers, options.helpers, container);
container.helpers = mergedHelpers;
if (templateSpec.usePartial) {
@@ -355,21 +349,21 @@ export function wrapProgram(
export function resolvePartial(partial, context, options) {
if (!partial) {
if (options.name === '@partial-block') {
partial = options.data['partial-block'];
partial = lookupOwnProperty(options.data, 'partial-block');
} else {
partial = options.partials[options.name];
partial = lookupOwnProperty(options.partials, options.name);
}
} else if (!partial.call && !options.name) {
// This is a dynamic partial that returned a string
options.name = partial;
partial = options.partials[partial];
partial = lookupOwnProperty(options.partials, partial);
}
return partial;
}
export function invokePartial(partial, context, options) {
// Use the current closure context to save the partial-block if this partial
const currentPartialBlock = options.data && options.data['partial-block'];
const currentPartialBlock = lookupOwnProperty(options.data, 'partial-block');
options.partial = true;
if (options.ids) {
options.data.contextPath = options.ids[0] || options.data.contextPath;
@@ -410,6 +404,12 @@ export function noop() {
return '';
}
function lookupOwnProperty(obj, name) {
if (obj && Object.prototype.hasOwnProperty.call(obj, name)) {
return obj[name];
}
}
function initData(context, data) {
if (!data || !('root' in data)) {
data = data ? createFrame(data) : {};
@@ -435,9 +435,10 @@ function executeDecorators(fn, prog, container, depths, data, blockParams) {
return prog;
}
function wrapHelpersToPassLookupProperty(mergedHelpers, container) {
Object.keys(mergedHelpers).forEach(helperName => {
let helper = mergedHelpers[helperName];
function addHelpers(mergedHelpers, helpers, container) {
if (!helpers) return;
Object.keys(helpers).forEach(helperName => {
let helper = helpers[helperName];
mergedHelpers[helperName] = passLookupPropertyOption(helper, container);
});
}
@@ -445,6 +446,7 @@ function wrapHelpersToPassLookupProperty(mergedHelpers, container) {
function passLookupPropertyOption(helper, container) {
const lookupProperty = container.lookupProperty;
return wrapHelper(helper, options => {
return Utils.extend({ lookupProperty }, options);
options.lookupProperty = lookupProperty;
return options;
});
}
+45 -8
View File
@@ -196,16 +196,24 @@ module.exports.cli = function(opts) {
const objectName = opts.partial ? 'Handlebars.partials' : 'templates';
if (opts.namespace && !isValidNamespace(opts.namespace)) {
throw new Handlebars.Exception('Invalid namespace format');
}
let output = new SourceNode();
if (!opts.simple) {
if (opts.amd) {
const runtimeModulePath =
(opts.handlebarPath || '') + 'handlebars.runtime';
output.add(
"define(['" +
opts.handlebarPath +
'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];'
'define([' +
quoteForJavaScript(runtimeModulePath) +
'], function(Handlebars) {\n Handlebars = Handlebars["default"];'
);
} else if (opts.commonjs) {
output.add('var Handlebars = require("' + opts.commonjs + '");');
output.add(
'var Handlebars = require(' + quoteForJavaScript(opts.commonjs) + ');'
);
} else {
output.add('(function() {\n');
}
@@ -255,9 +263,9 @@ module.exports.cli = function(opts) {
}
output.add([
objectName,
"['",
template.name,
"'] = template(",
'[',
quoteForJavaScript(template.name),
'] = template(',
precompiled,
');\n'
]);
@@ -277,7 +285,9 @@ module.exports.cli = function(opts) {
}
if (opts.map) {
output.add('\n//# sourceMappingURL=' + opts.map + '\n');
output.add(
'\n//# sourceMappingURL=' + sanitizeSourceMapComment(opts.map) + '\n'
);
}
output = output.toStringWithSourceMap();
@@ -307,6 +317,33 @@ function arrayCast(value) {
return value;
}
/*
* Safely quotes a value for embedding in generated JavaScript strings
*
* Uses JSON.stringify which handles all special characters.
*/
function quoteForJavaScript(value) {
return JSON.stringify(String(value));
}
/**
* Validates that a namespace is a legitimate dotted JavaScript identifier
* (e.g. "App.templates") to prevent arbitrary code injection
*/
function isValidNamespace(namespace) {
return /^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(
namespace
);
}
/**
* Strips line terminators from source map URLs to prevent injection of new
* JavaScript lines via the sourceMappingURL comment
*/
function sanitizeSourceMapComment(value) {
return String(value).replace(/[\r\n\u2028\u2029]/g, '');
}
/**
* Run uglify to minify the compiled template, if uglify exists in the dependencies.
*
+5840 -13874
View File
File diff suppressed because it is too large Load Diff
+8 -11
View File
@@ -1,9 +1,9 @@
{
"name": "handlebars",
"barename": "handlebars",
"version": "4.7.7",
"version": "4.7.9",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "https://www.handlebarsjs.com/",
"homepage": "https://handlebarsjs.com/",
"keywords": [
"handlebars",
"mustache",
@@ -30,7 +30,7 @@
"uglify-js": "^3.1.4"
},
"devDependencies": {
"@playwright/test": "^1.17.1",
"@playwright/test": "1.44.1",
"aws-sdk": "^2.1.49",
"babel-loader": "^5.0.0",
"babel-runtime": "^5.1.10",
@@ -39,7 +39,6 @@
"chai-diff": "^1.0.1",
"concurrently": "^5.0.0",
"dirty-chai": "^2.0.1",
"dtslint": "^0.5.5",
"dustjs-linkedin": "^2.0.2",
"eco": "~1.1.0-rc-3",
"eslint": "^6.7.2",
@@ -47,9 +46,8 @@
"eslint-plugin-compat": "^3.13.0",
"eslint-plugin-es5": "^1.4.1",
"fs-extra": "^8.1.0",
"grunt": "^1.0.4",
"grunt": "1.5.3",
"grunt-babel": "^5.0.0",
"grunt-bg-shell": "^2.3.3",
"grunt-cli": "^1",
"grunt-contrib-clean": "^1",
"grunt-contrib-concat": "^1",
@@ -58,6 +56,7 @@
"grunt-contrib-requirejs": "^1",
"grunt-contrib-uglify": "^1",
"grunt-contrib-watch": "^1.1.0",
"grunt-shell": "^4.0.0",
"grunt-webpack": "^1.0.8",
"husky": "^3.1.0",
"jison": "~0.3.0",
@@ -76,20 +75,18 @@
},
"main": "lib/index.js",
"types": "types/index.d.ts",
"browser": {
".": "./dist/cjs/handlebars.js",
"./runtime": "./dist/cjs/handlebars.runtime.js"
},
"browser": "./dist/cjs/handlebars.js",
"bin": {
"handlebars": "bin/handlebars"
},
"scripts": {
"build": "grunt build",
"release": "npm run build && grunt release",
"format": "prettier --write '**/*.js' && eslint --fix .",
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
"lint:eslint": "eslint --max-warnings 0 .",
"lint:prettier": "prettier --check '**/*.js'",
"lint:types": "dtslint types",
"lint:types": "tsc --noEmit --project types",
"test": "npm run test:mocha",
"test:mocha": "grunt build && grunt test",
"test:browser": "playwright test --config tests/browser/playwright.config.js tests/browser/spec.js",
+22 -1
View File
@@ -2,7 +2,28 @@
## Development
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.7...master)
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.9...master)
## v4.7.9 - March 26th, 2026
- fix: enable shell mode for spawn to resolve Windows EINVAL issue - e0137c2
- fix type "RuntimeOptions" also accepting string partials - eab1d14
- feat(types): set `hash` to be a `Record<string, any>` - de4414d
- fix non-contiguous program indices - 4512766
- refactor: rename i to startPartIndex - e497a35
- security: fix security issues - 68d8df5
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...v4.7.9)
## v4.7.8 - July 27th, 2023
- Make library compatible with workers (#1894) - 3d3796c
- Don't rely on Node.js global object (#1776) - 2954e7e
- Fix compiling of each block params in strict mode (#1855) - 30dbf04
- Fix rollup warning when importing Handlebars as ESM - 03d387b
- Fix bundler issue with webpack 5 (#1862) - c6c6bbb
- Use https instead of git for mustache submodule - 88ac068
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.7...v4.7.8)
## v4.7.7 - February 15th, 2021
+9
View File
@@ -324,6 +324,15 @@ describe('builtin helpers', function() {
);
});
it('each with block params and strict compilation', function() {
expectTemplate(
'{{#each goodbyes as |value index|}}{{index}}. {{value.text}}!{{/each}}'
)
.withCompileOptions({ strict: true })
.withInput({ goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }] })
.toCompileTo('0. goodbye!1. Goodbye!');
});
it('each object with @index', function() {
expectTemplate(
'{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'
+140
View File
@@ -128,6 +128,146 @@ describe('compiler', function() {
);
});
it('should reject AST with invalid PathExpression depth', function() {
shouldThrow(
function() {
Handlebars.compile({
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
path: {
type: 'PathExpression',
data: false,
depth: '0',
parts: ['this'],
original: 'this'
},
params: []
}
]
})();
},
Error,
'Invalid AST: PathExpression.depth must be an integer'
);
});
it('should reject AST with non-array PathExpression parts', function() {
shouldThrow(
function() {
Handlebars.compile({
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
path: {
type: 'PathExpression',
data: false,
depth: 0,
parts: 'this',
original: 'this'
},
params: []
}
]
})();
},
Error,
'Invalid AST: PathExpression.parts must be an array'
);
});
it('should reject AST with non-string PathExpression part', function() {
shouldThrow(
function() {
Handlebars.compile({
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
path: {
type: 'PathExpression',
data: false,
depth: 0,
parts: [1],
original: 'this'
},
params: []
}
]
})();
},
Error,
'Invalid AST: PathExpression.parts must only contain strings'
);
});
it('should reject AST with invalid BooleanLiteral value type', function() {
shouldThrow(
function() {
Handlebars.compile({
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
path: {
type: 'PathExpression',
data: false,
depth: 0,
parts: ['if'],
original: 'if'
},
params: [
{
type: 'BooleanLiteral',
value: 'true',
original: true
}
]
}
]
})();
},
Error,
'Invalid AST: BooleanLiteral.value must be a boolean'
);
});
it('should ignore loc metadata while validating AST nodes', function() {
equal(
Handlebars.compile({
type: 'Program',
meta: null,
loc: { source: 'fake', start: { line: 1, column: 0 } },
body: [{ type: 'ContentStatement', value: 'Hello' }]
})(),
'Hello'
);
});
it('should accept AST with valid NumberLiteral values', function() {
equal(
Handlebars.compile(Handlebars.parse('{{lookup this 1}}'))(['a', 'b']),
'b'
);
});
it('should accept AST with valid BooleanLiteral values', function() {
equal(
Handlebars.compile(Handlebars.parse('{{#if true}}ok{{/if}}'))({}),
'ok'
);
});
it('can pass through an empty string', function() {
equal(Handlebars.compile('')(), '');
});
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['bom'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["bom"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "a";
},"useData":true});
});
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = CustomNamespace.templates = CustomNamespace.templates || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+1 -1
View File
@@ -1,6 +1,6 @@
(function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
})();
+3 -3
View File
@@ -1,9 +1,9 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['firstTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["firstTemplate"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>1</div>";
},"useData":true});
templates['secondTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["secondTemplate"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>2</div>";
},"useData":true});
return templates;
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['artifacts/partial.template'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["artifacts/partial.template"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>Test Partial</div>";
},"useData":true});
});
+2 -2
View File
@@ -1,6 +1,6 @@
define(['some-path/handlebars.runtime'], function(Handlebars) {
define(["some-path/handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+3 -3
View File
@@ -1,9 +1,9 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = someNameSpace = someNameSpace || {};
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
return templates;
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['non.default.extension'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["non.default.extension"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>This is a test</div>";
},"useData":true});
});
+5 -5
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['known.helpers'] = template({"1":function(container,depth0,helpers,partials,data) {
return templates["known.helpers"] = template({"0":function(container,depth0,helpers,partials,data) {
var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return parent[propertyName];
@@ -8,8 +8,8 @@ return parent[propertyName];
return undefined
};
return " <div>Some known helper</div>\n"
+ ((stack1 = lookupProperty(helpers,"anotherHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"anotherHelper","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":3,"column":4},"end":{"line":5,"column":22}}})) != null ? stack1 : "");
},"2":function(container,depth0,helpers,partials,data) {
+ ((stack1 = lookupProperty(helpers,"anotherHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"anotherHelper","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":3,"column":4},"end":{"line":5,"column":22}}})) != null ? stack1 : "");
},"1":function(container,depth0,helpers,partials,data) {
return " <div>Another known helper</div>\n";
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
@@ -18,7 +18,7 @@ return parent[propertyName];
}
return undefined
};
return ((stack1 = lookupProperty(helpers,"someHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"someHelper","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":0},"end":{"line":6,"column":15}}})) != null ? stack1 : "");
return ((stack1 = lookupProperty(helpers,"someHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"someHelper","hash":{},"fn":container.program(0, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":0},"end":{"line":6,"column":15}}})) != null ? stack1 : "");
},"useData":true});
});
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return Handlebars.partials['partial.template'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return Handlebars.partials["partial.template"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>Test Partial</div>";
},"useData":true});
});
+82 -1
View File
@@ -182,7 +182,7 @@ describe('precompiler', function() {
return 'amd';
};
Precompiler.cli({ templates: [emptyTemplate], amd: true, partial: true });
equal(/return Handlebars\.partials\['empty'\]/.test(log), true);
equal(/return Handlebars\.partials\["empty"\]/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
it('should output multiple amd partials', function() {
@@ -405,4 +405,85 @@ describe('precompiler', function() {
});
});
});
describe('GHSA-xjpj-3mr7-gcpf: precompiler output escaping', function() {
var FullHandlebars = require('../dist/cjs/handlebars')['default'];
function runCliAndCaptureOutput(options) {
var output = '';
var oldLog = console.log;
console.log = function() {
output += Array.prototype.join.call(arguments, '');
};
try {
Precompiler.cli(options);
} finally {
console.log = oldLog;
}
return output;
}
it('should not inject raw template names into generated code', function() {
var output = runCliAndCaptureOutput({
templates: [
{
name: "evil'];global.__xjpjName=1;//",
source: ''
}
],
amd: true
});
expect(output).to.not.match(/\['evil'\];global\.__xjpjName=1/);
});
it('should not inject raw commonjs option values into generated code', function() {
var output = runCliAndCaptureOutput({
templates: [{ name: 'safe', source: '' }],
commonjs: 'handlebars");global.__xjpjCommon=1;//'
});
expect(output).to.not.match(
/require\("handlebars"\);global\.__xjpjCommon=1/
);
});
it('should reject invalid namespace expressions', function() {
expect(function() {
runCliAndCaptureOutput({
templates: [{ name: 'safe', source: '' }],
namespace: 'App.ns;global.__xjpjNamespace=1;//'
});
}).to.throw(/Invalid namespace/);
});
it('should sanitize sourceMappingURL comment values', function() {
var oldPrecompile = FullHandlebars.precompile;
var oldWriteFileSync = fs.writeFileSync;
FullHandlebars.precompile = function() {
return {
code: '""',
map: '{"version":3,"sources":[],"names":[],"mappings":""}'
};
};
fs.writeFileSync = function() {};
var output;
try {
output = runCliAndCaptureOutput({
templates: [{ name: 'safe', source: '' }],
map: 'good.js.map\n;global.__xjpjMap=1;//'
});
} finally {
FullHandlebars.precompile = oldPrecompile;
fs.writeFileSync = oldWriteFileSync;
}
expect(output).to.not.match(
/sourceMappingURL=[^\n]*\n;global\.__xjpjMap=1/
);
});
});
});
+10 -3
View File
@@ -54,6 +54,13 @@ describe('runtime', function() {
/Template was precompiled with an older version of Handlebars than the current runtime/
);
});
it('should safely resolve missing partial map entries', function() {
equal(
Handlebars.VM.resolvePartial(undefined, {}, { name: 'missing' }),
undefined
);
});
});
describe('#child', function() {
@@ -91,13 +98,13 @@ describe('runtime', function() {
it('should expose child template', function() {
var template = Handlebars.compile('{{#foo}}bar{{/foo}}');
// Calling twice to hit the non-compiled case.
equal(template._child(1)(), 'bar');
equal(template._child(1)(), 'bar');
equal(template._child(0)(), 'bar');
equal(template._child(0)(), 'bar');
});
it('should render depthed content', function() {
var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}');
// Calling twice to hit the non-compiled case.
equal(template._child(1, undefined, [], [{ bar: 'baz' }])(), 'baz');
equal(template._child(0, undefined, [], [{ bar: 'baz' }])(), 'baz');
});
});
+155
View File
@@ -133,11 +133,13 @@ describe('security issues', function() {
'{{__defineGetter__}}',
'{{__defineSetter__}}',
'{{__lookupGetter__}}',
'{{__lookupSetter__}}',
'{{__proto__}}',
'{{lookup this "constructor"}}',
'{{lookup this "__defineGetter__"}}',
'{{lookup this "__defineSetter__"}}',
'{{lookup this "__lookupGetter__"}}',
'{{lookup this "__lookupSetter__"}}',
'{{lookup this "__proto__"}}'
];
@@ -422,6 +424,159 @@ describe('security issues', function() {
.toCompileTo('c');
});
});
describe('GHSA-2qvq-rjwj-gvw9: partial resolution must not use polluted prototypes', function() {
if (!Handlebars.compile) {
return;
}
afterEach(function() {
delete Object.prototype.widget;
});
it('should not resolve partial names from Object.prototype', function() {
// eslint-disable-next-line no-extend-native
Object.prototype.widget = '<img src=x onerror="alert(1)">';
expect(function() {
Handlebars.compile('<div>{{> widget}}</div>')({});
}).to.throw(/could not be found/);
});
});
describe('GHSA-2w6w-674q-4c4q, GHSA-xhpv-hc6g-r9c6, GHSA-3mfm-83xf-c92r: untrusted AST inputs', function() {
if (!Handlebars.compile) {
return;
}
function createInjectedProgram() {
return {
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: {
open: false,
close: false
},
path: {
type: 'PathExpression',
data: false,
depth: 0,
parts: ['lookup'],
original: 'lookup'
},
params: [
{
type: 'PathExpression',
data: false,
depth: 0,
parts: [],
original: 'this'
},
{
type: 'NumberLiteral',
value: '{},{})) + (Function) + (({}',
original: 1
}
]
}
]
};
}
it('should reject AST NumberLiteral type confusion in compile()', function() {
expect(function() {
var template = Handlebars.compile(createInjectedProgram());
template({});
}).to.throw(/Invalid AST/);
});
it('should reject AST objects passed via dynamic partial lookup', function() {
expect(function() {
var template = Handlebars.compile('{{> (lookup . "payload")}}');
template({
payload: createInjectedProgram()
});
}).to.throw(/Invalid AST|could not be found/);
});
});
describe('GHSA-442j-39wm-28r2: lookup must return checked value', function() {
it('should use the validated value from lookupProperty() in compat mode', function() {
var input = { child: {} };
var readCount = 0;
Object.defineProperty(input, 'unstable', {
enumerable: true,
get: function() {
readCount++;
return readCount === 1 ? 'first-read' : 'second-read';
}
});
expectTemplate('{{#with child}}{{unstable}}{{/with}}')
.withInput(input)
.withCompileOptions({ compat: true })
.toCompileTo('first-read');
});
});
describe('GHSA-9cx6-37pm-9jff: malformed decorators should fail safely', function() {
if (!Handlebars.compile) {
return;
}
it('should throw a controlled error for unknown decorators', function() {
var template = Handlebars.compile('{{*notRegistered}}');
expect(function() {
template({});
}).to.throw(/Missing decorator|not registered/);
});
});
describe('GHSA-new: @partial-block must not resolve from polluted prototype', function() {
if (!Handlebars.compile) {
return;
}
afterEach(function() {
delete Object.prototype['partial-block'];
});
it('should not resolve @partial-block from Object.prototype', function() {
// eslint-disable-next-line no-extend-native
Object.prototype['partial-block'] = '<img src=x onerror="alert(1)">';
expect(function() {
Handlebars.compile('{{> @partial-block}}')({});
}).to.throw(/could not be found/);
});
it('should not resolve @partial-block from Object.prototype inside a partial', function() {
// eslint-disable-next-line no-extend-native
Object.prototype['partial-block'] = '<img src=x onerror="alert(1)">';
Handlebars.registerPartial('testPartial', '{{> @partial-block}}');
try {
expect(function() {
Handlebars.compile('{{> testPartial}}')({});
}).to.throw(/could not be found/);
} finally {
Handlebars.unregisterPartial('testPartial');
}
});
it('should still render legitimate @partial-block content', function() {
Handlebars.registerPartial('wrapper', '<div>{{> @partial-block}}</div>');
try {
var result = Handlebars.compile('{{#> wrapper}}hello{{/wrapper}}')({});
expect(result).to.equal('<div>hello</div>');
} finally {
Handlebars.unregisterPartial('wrapper');
}
});
});
});
function wrapToAdjustContainer(precompiledTemplateFunction) {
+2 -1
View File
@@ -23,7 +23,8 @@ async function execFileWithInheritedOutput(command, args) {
return new Promise((resolve, reject) => {
const resolvedCommand = preferLocalDependencies(command);
const child = childProcess.spawn(resolvedCommand, args, {
stdio: 'inherit'
stdio: 'inherit',
shell: process.platform === 'win32' // Workaround for CVE-2024-27980
});
child.on('exit', code => {
if (code !== 0) {
+1 -2
View File
@@ -9,6 +9,5 @@ Execute the following commands in the project root:
```bash
npm install
npx grunt prepare
docker pull mcr.microsoft.com/playwright:focal
docker run -it --rm --volume $(pwd):/srv/app --workdir /srv/app --ipc=host mcr.microsoft.com/playwright:focal npm run test:browser
docker run -it --rm --volume $(pwd):/srv/app --workdir /srv/app --ipc=host mcr.microsoft.com/playwright:v1.44.1-jammy npm run test:browser
```
+1
View File
@@ -2,6 +2,7 @@ const { devices } = require('@playwright/test');
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
testMatch: ['spec.js'],
projects: [
{
name: 'chromium',
+10 -10
View File
@@ -7,27 +7,27 @@ async function waitForMochaAndAssertResult(page) {
expect(mochaResults.failures).toBe(0);
}
test('Spec handlebars.js', async ({ page, baseURL }) => {
await page.goto(`${baseURL}/spec/?headless=true`);
test('Spec handlebars.js', async ({ page }) => {
await page.goto(`/spec/?headless=true`);
await waitForMochaAndAssertResult(page);
});
test('Spec handlebars.amd.js (AMD)', async ({ page, baseURL }) => {
await page.goto(`${baseURL}/spec/amd.html?headless=true`);
test('Spec handlebars.amd.js (AMD)', async ({ page }) => {
await page.goto(`/spec/amd.html?headless=true`);
await waitForMochaAndAssertResult(page);
});
test('Spec handlebars.runtime.amd.js (AMD)', async ({ page, baseURL }) => {
await page.goto(`${baseURL}/spec/amd-runtime.html?headless=true`);
test('Spec handlebars.runtime.amd.js (AMD)', async ({ page }) => {
await page.goto(`/spec/amd-runtime.html?headless=true`);
await waitForMochaAndAssertResult(page);
});
test('Spec handlebars.js (UMD)', async ({ page, baseURL }) => {
await page.goto(`${baseURL}/spec/umd.html?headless=true`);
test('Spec handlebars.js (UMD)', async ({ page }) => {
await page.goto(`/spec/umd.html?headless=true`);
await waitForMochaAndAssertResult(page);
});
test('Spec handlebars.runtime.js (UMD)', async ({ page, baseURL }) => {
await page.goto(`${baseURL}/spec/umd-runtime.html?headless=true`);
test('Spec handlebars.runtime.js (UMD)', async ({ page }) => {
await page.goto(`/spec/umd-runtime.html?headless=true`);
await waitForMochaAndAssertResult(page);
});
+1 -1
View File
@@ -14,7 +14,7 @@ cd "$( dirname "$( readlink -f "$0" )" )" || exit 1
unset npm_config_prefix
echo "Handlebars should be able to run in various versions of NodeJS"
for node_version_to_test in 0.10 0.12 4 5 6 7 8 9 10 11 12 13 14 15 16 17; do
for node_version_to_test in 0.10 0.12 4 5 6 7 8 9 10 11 12 13 14 15 16 18; do
rm target node_modules package-lock.json -rf
mkdir target
+1 -1
View File
@@ -4,7 +4,7 @@ set -e
# Cleanup: package-lock and "npm ci" is not working with local dependencies
rm dist package-lock.json -rf
npm install
npm install --legacy-peer-deps
npm run build
for i in dist/*-test.js ; do
+3 -9
View File
@@ -1,6 +1,6 @@
{
"name": "webpack-test",
"description": "Various tests with Handlebars and Webpack",
"description": "Various tests with Handlebars and multiple webpack versions",
"version": "1.0.0",
"main": "index.js",
"scripts": {
@@ -8,14 +8,8 @@
"test": "node dist/main.js"
},
"private": true,
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"dependencies": {
"handlebars": "file:../../..",
"handlebars-loader": "^1.7.1",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7"
"handlebars-loader": "^1.7.1"
}
}
@@ -0,0 +1,5 @@
import Handlebars from 'handlebars/lib/handlebars';
import { assertEquals } from './lib/assert';
const template = Handlebars.compile('Author: {{author}}');
assertEquals(template({ author: 'Yehuda' }), 'Author: Yehuda');
@@ -0,0 +1,43 @@
import * as Handlebars from 'handlebars/runtime';
import { assertEquals } from './lib/assert';
const template = Handlebars.template({
compiler: [8, '>= 4.3.0'],
main: function(container, depth0, helpers, partials, data) {
var helper,
lookupProperty =
container.lookupProperty ||
function(parent, propertyName) {
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return parent[propertyName];
}
return undefined;
};
return (
'Author: ' +
container.escapeExpression(
((helper =
(helper =
lookupProperty(helpers, 'author') ||
(depth0 != null ? lookupProperty(depth0, 'author') : depth0)) !=
null
? helper
: container.hooks.helperMissing),
typeof helper === 'function'
? helper.call(depth0 != null ? depth0 : container.nullContext || {}, {
name: 'author',
hash: {},
data: data,
loc: {
start: { line: 1, column: 8 },
end: { line: 1, column: 18 }
}
})
: helper)
)
);
},
useData: true
});
assertEquals(template({ author: 'Yehuda' }), 'Author: Yehuda');
+20 -9
View File
@@ -2,15 +2,26 @@
set -e
run_tests () {
for i in dist/*-test.js ; do
echo "----------------------"
echo "-- Running $i"
echo "----------------------"
node "$i"
echo "Success"
done
}
# Cleanup: package-lock and "npm ci" is not working with local dependencies
rm dist package-lock.json -rf
npm install
npm run build
npm install --legacy-peer-deps
for i in dist/*-test.js ; do
echo "----------------------"
echo "-- Running $i"
echo "----------------------"
node "$i"
echo "Success"
done
# Test with webpack 4
npm install --legacy-peer-deps --no-save webpack@^4 webpack-cli@^3
npm run build
run_tests
# Test with webpack 5
npm install --legacy-peer-deps --no-save webpack@^5 webpack-cli@^4
npm run build
run_tests
+3 -3
View File
@@ -25,7 +25,7 @@ declare namespace Handlebars {
partial?: boolean;
depths?: any[];
helpers?: { [name: string]: Function };
partials?: { [name: string]: HandlebarsTemplateDelegate };
partials?: { [name: string]: Template };
decorators?: { [name: string]: Function };
data?: any;
blockParams?: any[];
@@ -39,7 +39,7 @@ declare namespace Handlebars {
export interface HelperOptions {
fn: TemplateDelegate;
inverse: TemplateDelegate;
hash: any;
hash: Record<string, any>;
data?: any;
}
@@ -60,7 +60,7 @@ declare namespace Handlebars {
export function unregisterHelper(name: string): void;
export function registerPartial(name: string, fn: Template): void;
export function registerPartial(spec: { [name: string]: HandlebarsTemplateDelegate }): void;
export function registerPartial(spec: { [name: string]: Template }): void;
export function unregisterPartial(name: string): void;
// TODO: replace Function with actual signature
+3
View File
@@ -249,6 +249,9 @@ function testProtoAccessControlControlOptions() {
allowedProtoProperties: { allowedProperty: true, forbiddenProperty: false },
allowProtoMethodsByDefault: true,
allowProtoPropertiesByDefault: false,
partials: {
link: '<a href="/people/{{id}}">{{name}}</a>'
}
}
);
}