Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
enketo-core
Advanced tools
The engine that powers Enketo Express and various third party tools including this selection.
Enketo's form engine is compatible with tools in the ODK ecosystem and complies with its XForms specification though not all features in that specification have been implemented yet.
This repo is meant to be used as a building block for any Enketo-powered application. See this page for a schematic overview of a real-life full-fledged data collection application and how Enketo Core fits into this.
The following browsers are officially supported:
We have to admit we do not test on all of these, but are committed to fixing browser-specific bugs that are reported for these browsers. Naturally, older browsers versions will often work as well - they are just not officially supported.
See graphs
npm install enketo-core --save
or include as a git submodule.media="print"
).// assumes the enketo-core package is mapped from the node_modules folder
import { Form } from 'enketo-core';
// The XSL transformation result contains a HTML Form and XML instance.
// These can be obtained dynamically on the client, or at the server/
// In this example we assume the HTML was injected at the server and modelStr
// was injected as a global variable inside a <script> tag.
// required HTML Form DOM element
const formEl = document.querySelector('form.or');
// required object containing data for the form
const data = {
// required string of the default instance defined in the XForm
modelStr: globalXMLInstance,
// optional string of an existing instance to be edited
instanceStr: null,
// optional boolean whether this instance has ever been submitted before
submitted: false,
// optional array of external data objects containing:
// {id: 'someInstanceId', xml: XMLDocument}
external: [],
// optional object of session properties
// 'deviceid', 'username', 'email', 'phonenumber', 'simserial', 'subscriberid'
session: {},
};
// Form-specific configuration
const options = {};
// Instantiate a form, with 2 parameters
const form = new Form(formEl, data, options);
// Initialize the form and capture any load errors
let loadErrors = form.init();
// If desired, scroll to a specific question with any XPath location expression,
// and aggregate any loadErrors.
loadErrors = loadErrors.concat(form.goTo('//repeat[3]/node'));
// submit button handler for validate button
$('#submit').on('click', function () {
// clear non-relevant questions and validate
form.validate().then(function (valid) {
if (!valid) {
alert('Form contains errors. Please see fields marked in red.');
} else {
// Record is valid!
const record = form.getDataStr();
// reset the form view
form.resetView();
// reinstantiate a new form with the default model and no options
form = new Form(formSelector, { modelStr: modelStr }, {});
// do what you want with the record
}
});
});
Global configuration (per app) is done in config.json which is meant to be overridden by a config file in your own application (e.g. by using rollup).
The maps
configuration can include an array of Mapbox TileJSON objects (or a subset of these with at least a name
, tiles
(array) and an attribution
property, and optionally maxzoom
and minzoom
). You can also mix and match Google Maps layers. Below is an example of a mix of two map layers provided by OSM (in TileJSON format) and Google maps.
[
{
"name": "street",
"tiles": [ "https://tile.openstreetmap.org/{z}/{x}/{y}.png" ],
"attribution": "Map data © <a href=\"http://openstreetmap.org\">OpenStreetMap</a> contributors"
},
{
"name": "satellite",
"tiles": "GOOGLE_SATELLITE"
}
]
For GMaps layers you have the four options as tiles values: "GOOGLE_SATELLITE"
, "GOOGLE_ROADMAP"
, "GOOGLE_HYBRID"
, "GOOGLE_TERRAIN"
. You can also add other TileJSON properties, such as minZoom, maxZoom, id to all layers.
The Google API key that is used for geocoding (in the geo widgets' search box). Can be obtained here. Make sure to enable the GeoCoding API service. If you are using Google Maps layers, the same API key is used. Make sure to enable the Google Maps JavaScript API v3 service as well in that case (see next item).
This setting with the default false
value determines whether Enketo should validate questions immediately if a related value changes. E.g. if question A has a constraint that depends on question B, this mode would re-validate question A if the value for question B changes. This mode will slow down form traversal. When set to false
that type of validation is only done at the end when the Submit button is clicked or in Pages mode when the user clicks Next.
This setting with default true
value determines whether the Next button should trigger validation of the current page and block the user from moving to the next page if validation fails.
This setting with default true
value determines whether to enable support for swiping to the next and previous page for forms that are divided into pages.
Per-form configuration is done by adding an (optional) options object as 3rd parameter when instantiating a form.
If printRelevantOnly
is set to true
or not set at all, printing the form only includes what is visible, ie. all the groups and questions that do not have a relevant
expression or for which the expression evaluates to true
.
new Form(formselector, data, {
printRelevantOnly: false
});
The language
option overrides the default languages rules of the XForm itself. Pass any valid and present-in-the-form IANA subtag string, e.g. ar
.
yarn install
grunt
(yarn grunt
, npx grunt
)yarn start
xform
queryparameter or load a local from the /tests/forms folder in this repoyarn test
(headless chrome) and yarn run test-browsers
(browsers); note: running tests updates the coverage badge in README.md, but these changes should not be committed except when preparing a releasetouch=true
and reducing the window size allows you to simulate mobile touchscreenstest
task. You can also manually run grunt eslint:fix
to fix style issues.grunt karma
, headless: grunt karma:headless
, browsers: grunt karma:browsers
)yarn run test-watch
, and support for debugging in VSCode is provided. For instructions see Debugging test watch mode in VSCode belowBasic usage:
Optionally, you can add a keyboard shortcut to select launch tasks:
workbench.action.debug.selectandstart
The core can be fairly easily extended with alternative themes. See the plain, the grid, and the formhub themes already included in /src/sass. We would be happy to discuss whether your contribution should be a part of the core, the default theme or be turned into a new theme.
For custom themes that go beyond just changing colors and fonts, keep in mind all the different contexts for a theme:
Widgets extend the Widget class. This is an example:
(see full functioning example at /src/widget/example/my-widget.js
import Widget from '../../js/widget';
/*
* Make sure to give the widget a unique widget class name and extend Widget.
*/
class MyWidget extends Widget {
/*
* The selector that determines on which form control the widget is instantiated.
* Make sure that any other widgets that target the same from control are not interfering with this widget by disabling
* the other widget or making them complementary.
* This function is always required.
*/
static get selector() {
return '.or-appearance-my-widget input[type="number"]';
}
/*
* Initialize the widget that has been instantiated using the Widget (super) constructor.
* The _init function is called by that super constructor unless that constructor is overridden.
* This function is always required.
*/
_init() {
// Hide the original input
this.element.classList.add('hide');
// Create the widget's DOM fragment.
const fragment = document.createRange().createContextualFragment(
`<div class="widget">
<input class="ignore" type="range" min="0" max="100" step="1"/>
<div>`
);
fragment.querySelector('.widget').appendChild(this.resetButtonHtml);
// Only when the new DOM has been fully created as a HTML fragment, we append it.
this.element.after(fragment);
const widget = this.element.parentElement.querySelector('.widget');
this.range = widget.querySelector('input');
// Set the current loaded value into the widget
this.value = this.originalInputValue;
// Set event handlers for the widget
this.range.addEventListener('change', this._change.bind(this));
widget
.querySelector('.btn-reset')
.addEventListener('click', this._reset.bind(this));
// This widget initializes synchronously so we don't return anything.
// If the widget initializes asynchronously return a promise that resolves to `this`.
}
_reset() {
this.value = '';
this.originalInputValue = '';
this.element.classList.add('empty');
}
_change(ev) {
// propagate value changes to original input and make sure a change event is fired
this.originalInputValue = ev.target.value;
this.element.classList.remove('empty');
}
/*
* Disallow user input into widget by making it readonly.
*/
disable() {
this.range.disabled = true;
}
/*
* Performs opposite action of disable() function.
*/
enable() {
this.range.disabled = false;
}
/*
* Update the language, list of options and value of the widget.
*/
update() {
this.value = this.originalInputValue;
}
/*
* Obtain the current value from the widget. Usually required.
*/
get value() {
return this.element.classList.contains('empty') ? '' : this.range.value;
}
/*
* Set a value in the widget. Usually required.
*/
set value(value) {
this.range.value = value;
}
}
export default MyWidget;
Some of the tests are common to all widgets, and can be run with a few lines:
(see full functioning example at /test/spec/widget.example.spec.js)
import ExampleWidget from '../../src/widget/example/my-widget';
import { runAllCommonWidgetTests } from '../helpers/testWidget';
const FORM = `<label class="question or-appearance-my-widget">
<input type="number" name="/data/node">
</label>`;
const VALUE = '2';
runAllCommonWidgetTests(ExampleWidget, FORM, VALUE);
_init
function to your widget that either returns nothing or a Promise (if it initializes asynchronously)this.originalInputValue
into the widgetthis.originalInputValue = ...
applyfocus
event on the original input and focus the widgetwidget
css class to the top level elements it adds to the DOM (but not to their children)ignore
class to isolate them from the Enketo form engineenable()
, disable()
and update()
method overrides. See the Widget class.condition()
function in Widget.js.fakefocus
event to the original input when the widget gets focus (rarely required, but see rank widget)Fired on a form control when it is programmatically updated and when this results in a change in value
Fired on a form control when it is updated directly by the user and when this results in a change in value
Fired on a form control when it has failed constraint, datatype, or required validation.
Fired on model.$events, when a single model value has changed its value, a repeat is added, or a node is removed. It passes an "update object". This event is propagated for external use by firing it on the form.or element as well.
Fired on model.events when a new record (instance) is loaded for the first time. It's described here: odk-instance-first-load.
Fired on a newly added repeat. It's described here: odk-instance-first-load.
Fired on the repeat or repeat element immediately following a removed repeat.
Fired on model.events, when a node is removed. It passes an "update object". This event is propagated for external use by firing it on the form.or element as well.
Fired on form control when an attempt is made to 'go to' this field but it is hidden from view because it is non-relevant.
Fired on form control when an attempt is made to 'go to' this field but it is hidden from view because it is has no form control.
Fired when user flips to a new page, on the page element itself.
Fired on form.or element when user makes first edit in form. Fires only once.
Fired on form.or element when validation completes.
Fired when the user moves to a different question in the form.
The development of this library is now led by ODK and funded by customers of the ODK Cloud hosted service.
Past sponsors include:
FAQs
Extensible Enketo form engine
The npm package enketo-core receives a total of 249 weekly downloads. As such, enketo-core popularity was classified as not popular.
We found that enketo-core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.