Skip to content

Commit

Permalink
Fix regex escaping, rename to enableRegex, and clean up documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
aslagle committed Jan 29, 2015
1 parent 097b51b commit e800bfc
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 27 deletions.
39 changes: 21 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ If you're updating to Meteor 0.8.0, note that reactiveTable is now a template wi
- [Using events](#using-events)
- [Multiple tables](#multiple-tables)
- [Server-side pagination and filtering](#server-side-pagination-and-filtering-beta)
- [Server-side Settings](#server-side-settings)
- [Internationalization](#internationalization)

## Quick Start
Expand Down Expand Up @@ -68,6 +69,7 @@ The reactiveTable helper accepts additional arguments that can be used to config
* `fields`: Object. Controls the columns; see below.
* `showColumnToggles`: Boolean. Adds a 'Columns' button to the top right that allows the user to toggle which columns are displayed. (Note: there aren't translations for this button yet - please [add one](#internationalization) if you're using it.) Add `hidden` to fields to hide them unless toggled on, see below. Default `false`.
* `useFontAwesome`: Boolean. Whether to use [Font Awesome](http://fortawesome.github.io/Font-Awesome/) for icons. Requires the `fortawesome:fontawesome` package to be installed. Default `true` if `fortawesome:fontawesome` is installed, else `false`.
* `enableRegex`: Boolean. Whether to use filter text as a regular expression instead of a regular search term. When true, users won't be able to filter by special characters without escaping them. Default `false`. (Note: Setting this option on the client won't affect server-side filtering - see [Server-side pagination and filtering](#server-side-pagination-and-filtering-beta))
* `class`: String. Classes to add to the table element in addition to 'reactive-table'. Default: 'table table-striped table-hover col-sm-12'.
* `id`: String. Unique id to add to the table element. Default: generated with [_.uniqueId](http://underscorejs.org/#uniqueId).
* `rowClass`: String or function returning a class name. The row element will be passed as first parameter.
Expand Down Expand Up @@ -274,7 +276,7 @@ Arguments:
- name: The name of the publication
- collection: A function that returns the collection to publish (or just a collection, if it's insecure).
- selector: (Optional) A function that returns mongo selector that will limit the results published (or just the selector).
- settings: (Optional) A object with settings on server side's publish function. (Details see below)
- settings: (Optional) A object with settings on server side's publish function. (Details below)

Inside the functions, `this` is the publish handler object as in [Meteor.publish](http://docs.meteor.com/#/full/meteor_publish), so `this.userId` is available.

Expand Down Expand Up @@ -310,30 +312,31 @@ if (Meteor.isServer) {

Other table settings should work normally, except that all fields will be sorted by value, even if using `fn`. The fields setting is required when using a server-side collection.

### Settings
### Server-side Settings

Possible options:
- autoQuoteSearchterm (Boolean - *default=* **true**):
This controlls if the searchterm should be automatically been qutoted for regex special chars. If you want to have pure RegEx possibility as searchterm, set this option to **false**.
The following options are available in the settings argument to ReactiveTable.publish:
- enableRegex (Boolean - *default=* **false**):
Whether to use filter text as a regular expression instead of a regular search term. When true, users will be able to enter regular expressions to filter the table, but your application may be vulnerable to a [ReDoS](http://en.wikipedia.org/wiki/ReDoS) attack. Also, when true, users won't be able to use special characters in filter text without escaping them.

Examples:
in clients Reactive Table input field searchterm: "me + you"
A user filters with "me + you"
```JavaScript
// Publish only the current user's items (server side)
ReactiveTable.publish("user-items", Items, function () {
return {"userId": this.userId};
}, {autoQuoteSearchterm: true});
ReactiveTable.publish(
"some-items",
Items,
{"show": true}
{"enableRegex": false});
```
will provide you search results, while
```JavaScript
// Publish only the current user's items (server side)
ReactiveTable.publish("user-items", Items, function () {
return {"userId": this.userId};
}, {autoQuoteSearchterm: false});

```
will crash on server side, since "me + you" is not a valid regex ("me \\+ you" would be correct).
> Default is to automatic quote the searchterm, since most users wont 'speak' regex and just type in a searchterm.
ReactiveTable.publish(
"some-items",
Items,
{"show": true}
{"enableRegex": true});
```
will crash on the server, since "me + you" is not a valid regex ("me \\+ you" would be correct).
> Default is to disable regex and automatically escape the term, since most users wont 'speak' regex and just type in a search term.
## Internationalization

Expand Down
12 changes: 6 additions & 6 deletions lib/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,25 @@ var parseFilterString = function (filterString) {
return filters;
};

var quoteRegex = function(text) {
var escapeRegex = function(text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
};

getFilterQuery = function (filter, fields, settings) {
settings = settings || {};
if (settings.autoQuoteSearchterm === undefined) {
settings.autoQuoteSearchterm = true;
if (settings.enableRegex === undefined) {
settings.enableRegex = false;
}
var numberRegExp = /^\d+$/;
var queryList = [];
if (filter) {
var filters = parseFilterString(filter);
_.each(filters, function (filterWord) {
if (settings.enableRegex === false) {
filterWord = escapeRegex(filterWord);
}
var filterQueryList = [];
_.each(fields, function (field) {
if (settings.autoQuoteSearchterm === true) {
filterWord = quoteRegex(filterWord);
}
var filterRegExp = new RegExp(filterWord, 'i');
var query = {};
query[field.key || field] = filterRegExp;
Expand Down
6 changes: 4 additions & 2 deletions lib/reactive_table.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ var setup = function () {
} else {
context.useFontAwesome = this.data.useFontAwesome;
}
context.enableRegex = getDefaultFalseSetting('enableRegex', this.data);

context.ready = new ReactiveVar(true);

if (context.server) {
Expand All @@ -249,7 +251,7 @@ var getPageCount = function () {
count = ReactiveTableCounts.findOne(this.publicationId.get());
return Math.ceil((count ? count.count : 0) / rowsPerPage);
} else {
var filterQuery = getFilterQuery(this.filter.get(), this.fields);
var filterQuery = getFilterQuery(this.filter.get(), this.fields, {enableRegex: this.enableRegex});
count = this.collection.find(filterQuery).count();
return Math.ceil(count / rowsPerPage);
}
Expand Down Expand Up @@ -345,7 +347,7 @@ Template.reactiveTable.helpers({
var limit = this.rowsPerPage.get();
var currentPage = this.currentPage.get();
var skip = currentPage * limit;
var filterQuery = getFilterQuery(this.filter.get(), this.fields);
var filterQuery = getFilterQuery(this.filter.get(), this.fields, {enableRegex: this.enableRegex});

if (sortKeyField.fn && !sortKeyField.sortByValue) {
var data = this.collection.find(filterQuery).fetch();
Expand Down
3 changes: 2 additions & 1 deletion package.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package.describe({
summary: "A reactive table designed for Meteor",
version: "0.6.1",
version: "0.6.2",
name: "aslagle:reactive-table",
git: "https://github.com/aslagle/reactive-table.git"
});
Expand Down Expand Up @@ -53,6 +53,7 @@ Package.on_test(function (api) {
api.add_files('test/test_reactivity.html', 'client');
api.add_files('test/test_reactivity.js', 'client');
api.add_files('test/test_sorting.js', 'client');
api.add_files('test/test_filtering_server.js', 'server');
api.add_files('test/test_filtering.js', 'client');
api.add_files('test/test_pagination.js', 'client');
api.add_files('test/test_i18n.js', 'client');
Expand Down
112 changes: 112 additions & 0 deletions test/test_filtering.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,115 @@ testAsyncMulti('Filtering - server-side', [function (test, expect) {

Meteor.setTimeout(expectSixRows, 500);
}]);

testAsyncMulti('Filtering - enableRegex false client side', [function (test, expect) {
var rows = [
{name: 'item 1', value: '1+2'},
{name: 'item 2', value: 'abc'}
];

var table = Blaze.renderWithData(
Template.reactiveTable,
{collection: rows},
document.body
);
test.length($('.reactive-table tbody tr'), 2, "initial two rows");

var expectOneRow = expect(function () {
test.length($('.reactive-table tbody tr'), 1, "filtered to one row");
test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "item 1", "filtered first row");
Blaze.remove(table);
});

$('.reactive-table-filter input').val('+');
$('.reactive-table-filter input').trigger('input');
Meteor.setTimeout(expectOneRow, 1000);
}]);

testAsyncMulti('Filtering - enableRegex true client side', [function (test, expect) {
var rows = [
{name: 'item 1', value: '1+2'},
{name: 'item 2', value: 'abc'}
];

var table = Blaze.renderWithData(
Template.reactiveTable,
{collection: rows, enableRegex: true},
document.body
);
test.length($('.reactive-table tbody tr'), 2, "initial two rows");

var expectTwoRows = expect(function () {
test.length($('.reactive-table tbody tr'), 2, "filtered to both rows");
Blaze.remove(table);
});

var expectOneRow = expect(function () {
test.length($('.reactive-table tbody tr'), 1, "filtered to one row");
test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "item 1", "filtered first row");

$('.reactive-table-filter input').val('a|1');
$('.reactive-table-filter input').trigger('input');
Meteor.setTimeout(expectTwoRows, 1000);
});

$('.reactive-table-filter input').val('\\+');
$('.reactive-table-filter input').trigger('input');
Meteor.setTimeout(expectOneRow, 1000);
}]);

testAsyncMulti('Filtering - enableRegex false server side', [function (test, expect) {
var table = Blaze.renderWithData(
Template.reactiveTable,
{collection: 'filter-regex-disabled', fields: ['name', 'value']},
document.body
);

var expectOneRow = expect(function () {
test.length($('.reactive-table tbody tr'), 1, "filtered to one row");
test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "item 1", "filtered first row");
Blaze.remove(table);
});

var expectInitialRows = expect(function () {
test.length($('.reactive-table tbody tr'), 2, "initial two rows");

$('.reactive-table-filter input').val('+');
$('.reactive-table-filter input').trigger('input');
Meteor.setTimeout(expectOneRow, 1000);
});

Meteor.setTimeout(expectInitialRows, 500);
}]);

testAsyncMulti('Filtering - enableRegex true server side', [function (test, expect) {
var table = Blaze.renderWithData(
Template.reactiveTable,
{collection: 'filter-regex-enabled', fields: ['name', 'value']},
document.body
);

var expectTwoRows = expect(function () {
test.length($('.reactive-table tbody tr'), 2, "filtered to both rows");
Blaze.remove(table);
});

var expectOneRow = expect(function () {
test.length($('.reactive-table tbody tr'), 1, "filtered to one row");
test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "item 1", "filtered first row");

$('.reactive-table-filter input').val('a|1');
$('.reactive-table-filter input').trigger('input');
Meteor.setTimeout(expectTwoRows, 1000);
});

var expectInitialRows = expect(function () {
test.length($('.reactive-table tbody tr'), 2, "initial two rows");

$('.reactive-table-filter input').val('\\+');
$('.reactive-table-filter input').trigger('input');
Meteor.setTimeout(expectOneRow, 1000);
});

Meteor.setTimeout(expectInitialRows, 500);
}]);
9 changes: 9 additions & 0 deletions test/test_filtering_server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
var collection = new Meteor.Collection('filter-regex-test');

collection.remove({});

collection.insert({name: 'item 1', value: '1+2'});
collection.insert({name: 'item 2', value: 'abc'});

ReactiveTable.publish('filter-regex-disabled', collection, {}, {enableRegex: false});
ReactiveTable.publish('filter-regex-enabled', collection, {}, {enableRegex: true});

0 comments on commit e800bfc

Please sign in to comment.