[libzonemaster-webbackend-perl] 01/02: New upstream version 1.1.0

Ondřej Surý ondrej at debian.org
Fri Jun 23 07:50:11 UTC 2017


This is an automated email from the git hooks/post-receive script.

ondrej pushed a commit to branch master
in repository libzonemaster-webbackend-perl.

commit f099ab440966a6b9ad744034d46524ff944b295a
Author: Ondřej Surý <ondrej at sury.org>
Date:   Fri Jun 23 09:39:04 2017 +0200

    New upstream version 1.1.0
---
 CHANGES                                    |   32 +
 MANIFEST                                   |   36 +-
 META.yml                                   |   54 ++
 MYMETA.json                                |   76 ++
 MYMETA.yml                                 |   53 +
 README.md                                  |   49 +-
 docs/API.md                                | 1443 +++++++++++++++++++---------
 docs/Architecture.md                       |   86 ++
 docs/GettingStarted.md                     |   47 +
 docs/TypographicConventions.md             |   25 +
 docs/installation.md                       |  629 ++++++++----
 docs/notes_api.md                          |  351 -------
 docs/upgrade-from-1.0.x-to-1.1.x.md        |   13 +
 inc/Module/Install.pm                      |  474 +++++++++
 inc/Module/Install/Base.pm                 |   83 ++
 inc/Module/Install/Can.pm                  |  154 +++
 inc/Module/Install/Fetch.pm                |   93 ++
 inc/Module/Install/Makefile.pm             |  418 ++++++++
 inc/Module/Install/Metadata.pm             |  722 ++++++++++++++
 inc/Module/Install/Scripts.pm              |   29 +
 inc/Module/Install/Share.pm                |   96 ++
 inc/Module/Install/Win32.pm                |   64 ++
 inc/Module/Install/WriteAll.pm             |   63 ++
 lib/Zonemaster/WebBackend.pm               |    9 +
 lib/Zonemaster/WebBackend/Config.pm        |   38 +-
 lib/Zonemaster/WebBackend/DB.pm            |   53 +-
 lib/Zonemaster/WebBackend/DB/MySQL.pm      |  100 +-
 lib/Zonemaster/WebBackend/DB/PostgreSQL.pm |   90 +-
 lib/Zonemaster/WebBackend/DB/SQLite.pm     |   26 +-
 lib/Zonemaster/WebBackend/Engine.pm        |   96 +-
 lib/Zonemaster/WebBackend/Runner.pm        |   39 +-
 lib/Zonemaster/WebBackend/Translator.pm    |    6 +-
 script/create_db_mysql.pl                  |    9 +
 script/create_db_postgresql_9.3.pl         |    9 +
 script/crontab_job_runner/execute_tests.pl |    4 +-
 script/zm_wb_daemon                        |    4 +-
 script/zonemaster_webbackend.psgi          |   14 +-
 share/backend_config.ini                   |    8 +-
 {docs => share}/cleanup-mysql.sql          |    0
 {docs => share}/cleanup-postgres.sql       |    0
 {docs => share}/initial-mysql.sql          |   11 +-
 {docs => share}/initial-postgres.sql       |    5 +
 share/travis_mysql_backend_config.ini      |    6 +-
 share/travis_sqlite_backend_config.ini     |    6 +-
 t/test01.t                                 |    2 +-
 t/test_DB_backend.pl                       |    2 +-
 t/test_validate_syntax.t                   |   18 -
 47 files changed, 4485 insertions(+), 1160 deletions(-)

diff --git a/CHANGES b/CHANGES
index 8cd0c0b..63a0272 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,4 +1,36 @@
 Revision history for Zonemaster Backend.
+v1.1.0 2016-12-08
+	Fixes #247 - Error while creating database in Ubuntu 16.0.4
+	Fixes #237 - Update installation instructions
+	Fixes #236 - Key/parameter "advanced" should be deprecated in the backend
+	Fixes #233 - API documentatuion improvments
+	Fixes #232 - Various improvements to the installation guide
+	Fixes #230 - Add lots of structure to the installation guide
+	Fixes #219 - Make IPv4 or IPv6 optionnal. If none are provided the engine will run with both enabled
+	Fixes #211 - Create a config_file parameter allowing a backend to execute only tests of a certain priority
+	Fixes #210 - Make IP adresses for nameservers optional
+	Fixes #207 - Batch never completes
+	Fixes #201 - Add IP '127.0.0.1' to the list of accepted IP addresses to the add_api_user API method
+	Fixes #200 - Syntac errors in the API.md documentation
+	Fixes #197 - Addeed an Empty WebBackend.pm module to the distribution
+	Fixes #196 - Fixes errors in marameters retruned by the backend with MySQL DB
+	Fixes #191 - The add_batch_job API takes hours to schedule several hundread thousand domains for thesting
+	Fixes #186 - Why is "ip" required in "start_domain_test" ?
+	Fixes #165 - .sql files belong in "share", not doc
+	Fixes #161 - Bug in Zonemaster/WebBackend/Engine.pm when validating parameters
+v1.0.7 2016-10-17
+	Fixes #168 - Published to CPAN
+v1.0.6 2016-10-11
+	Fixes #189 - Fixes s/cutom/custom/ typos
+	Fixes #188 - Fixes s/professes/processes/ typos
+	Fixes #185 - Document "add_api_user" and "add_batch_job" completely
+	Fixes #183 - Banckend doesn't pass "make test" on perl 5.24
+	Fixes #174 - Filters feature (allows to use this possibility of the engine in the backend APIs)
+	Fixes #159 - "make test" of zonemaster-backend fails with non-default locale under Ubuntu 14.04
+	Fixes #158 - Dates needs to be returned in UTC from the database
+	Fixes #156 - Add Docker based installation instructions
+	Fixes #155 - Change de preflight test to block only on Basic00
+	Fixes #153 - Improve the batch API (Fixed and added bulk testing methods)
 v1.0.5 2015-12-17
 	Fixes #148 - Use iana_profile.josn instead of iana.json as source file for IANA tests 
 	Fixes #141 - Database initialisation files (.sql) not updated with the new hash_id column
diff --git a/MANIFEST b/MANIFEST
index a2e76e5..b6d81ed 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,14 +1,19 @@
 .gitignore
+.travis.yml
 CHANGES
 CodeSnippets/client.pl
 CodeSnippets/Client.pm
-docs/cleanup-mysql.sql
-docs/cleanup-postgres.sql
+CodeSnippets/sample_api_call__get_data_from_parent_zone.pl
+CodeSnippets/sample_api_call__get_ns_ips.pl
+CodeSnippets/sample_api_call__validate_syntax.pl
+CodeSnippets/sample_api_call__version_info.pl
+docs/API.md
+docs/Architecture.md
 docs/files-description.md
-docs/initial-mysql.sql
-docs/initial-postgres.sql
+docs/GettingStarted.md
 docs/installation.md
-docs/notes_api.md
+docs/TypographicConventions.md
+docs/upgrade-from-1.0.x-to-1.1.x.md
 inc/Module/Install.pm
 inc/Module/Install/Base.pm
 inc/Module/Install/Can.pm
@@ -19,6 +24,7 @@ inc/Module/Install/Scripts.pm
 inc/Module/Install/Share.pm
 inc/Module/Install/Win32.pm
 inc/Module/Install/WriteAll.pm
+lib/Zonemaster/WebBackend.pm
 lib/Zonemaster/WebBackend/Config.pm
 lib/Zonemaster/WebBackend/DB.pm
 lib/Zonemaster/WebBackend/DB/MySQL.pm
@@ -35,14 +41,30 @@ META.yml
 MYMETA.json
 MYMETA.yml
 README.md
+script/add-test.pl
+script/create_db_mysql.pl
 script/create_db_postgresql_9.3.pl
+script/crontab_job_runner/execute_tests.pl
+script/crontab_job_runner/execute_zonemaster_P10.pl
+script/crontab_job_runner/execute_zonemaster_P5.pl
+script/patch_db_mysql_for_backend_DB_version_lower_than_1.0.3.pl
+script/patch_db_postgresq_for_backend_DB_version_lower_than_1.0.3.pl
 script/zm_wb_daemon
 script/zonemaster_webbackend.psgi
 share/backend_config.ini
+share/cleanup-mysql.sql
+share/cleanup-postgres.sql
+share/initial-mysql.sql
+share/initial-postgres.sql
 share/starman-zonemaster.conf
+share/travis_mysql_backend_config.ini
+share/travis_postgresql_backend_config.ini
+share/travis_sqlite_backend_config.ini
+share/zm-backend.sh
+share/zm-centos.sh
 share/zm_wb_daemon.conf
+t/test01.data
 t/test01.t
-t/test_mysql_backend.pl
-t/test_postgresql_backend.pl
+t/test_DB_backend.pl
 t/test_runner.pl
 t/test_validate_syntax.t
diff --git a/META.yml b/META.yml
new file mode 100644
index 0000000..beec5d4
--- /dev/null
+++ b/META.yml
@@ -0,0 +1,54 @@
+---
+abstract: 'The Zonemaster project Web Backend engine'
+author:
+  - 'Michal TOMA <toma at nic.fr>'
+build_requires:
+  DBD::SQLite: 0
+  ExtUtils::MakeMaker: 6.59
+configure_requires:
+  ExtUtils::MakeMaker: 6.59
+distribution_type: module
+dynamic_config: 1
+generated_by: 'Module::Install version 1.14'
+license: perl
+meta-spec:
+  url: http://module-build.sourceforge.net/META-spec-v1.4.html
+  version: 1.4
+name: Zonemaster-WebBackend
+no_index:
+  directory:
+    - CodeSnippets
+    - inc
+    - share
+    - t
+recommends:
+  DBD::Pg: 0
+  DBD::SQLite: 0
+  DBD::mysql: 0
+requires:
+  Config::IniFiles: 0
+  DBI: 1.616
+  Daemon::Control: 0
+  File::ShareDir: 0
+  File::Slurp: 0
+  HTML::Entities: 0
+  IO::CaptureOutput: 0
+  JSON::PP: 0
+  JSON::RPC::Dispatch: 0
+  LWP::UserAgent: 0
+  Locale::TextDomain: 0
+  Moose: 2.04
+  Moose::Role: 0
+  Net::IP::XS: 0.14
+  Net::LDNS: 0.72
+  Parallel::ForkManager: 1.12
+  Plack::Builder: 0
+  Plack::Middleware::Debug: 0
+  Router::Simple::Declare: 0
+  Starman: 0
+  String::ShellQuote: 0
+  Zonemaster: 0
+  perl: 5.14.2
+resources:
+  license: http://dev.perl.org/licenses/
+version: 1.1.0
diff --git a/MYMETA.json b/MYMETA.json
new file mode 100644
index 0000000..7ea458d
--- /dev/null
+++ b/MYMETA.json
@@ -0,0 +1,76 @@
+{
+   "abstract" : "The Zonemaster project Web Backend engine",
+   "author" : [
+      "Michal TOMA <toma at nic.fr>"
+   ],
+   "dynamic_config" : 0,
+   "generated_by" : "Module::Install version 1.14, CPAN::Meta::Converter version 2.143240",
+   "license" : [
+      "perl_5"
+   ],
+   "meta-spec" : {
+      "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
+      "version" : "2"
+   },
+   "name" : "Zonemaster-WebBackend",
+   "no_index" : {
+      "directory" : [
+         "CodeSnippets",
+         "inc",
+         "share",
+         "t"
+      ]
+   },
+   "prereqs" : {
+      "build" : {
+         "requires" : {
+            "DBD::SQLite" : "0",
+            "ExtUtils::MakeMaker" : "6.59"
+         }
+      },
+      "configure" : {
+         "requires" : {
+            "ExtUtils::MakeMaker" : "0"
+         }
+      },
+      "runtime" : {
+         "recommends" : {
+            "DBD::Pg" : "0",
+            "DBD::SQLite" : "0",
+            "DBD::mysql" : "0"
+         },
+         "requires" : {
+            "Config::IniFiles" : "0",
+            "DBI" : "1.616",
+            "Daemon::Control" : "0",
+            "File::ShareDir" : "0",
+            "File::Slurp" : "0",
+            "HTML::Entities" : "0",
+            "IO::CaptureOutput" : "0",
+            "JSON::PP" : "0",
+            "JSON::RPC::Dispatch" : "0",
+            "LWP::UserAgent" : "0",
+            "Locale::TextDomain" : "0",
+            "Moose" : "2.04",
+            "Moose::Role" : "0",
+            "Net::IP::XS" : "0.14",
+            "Net::LDNS" : "0.72",
+            "Parallel::ForkManager" : "1.12",
+            "Plack::Builder" : "0",
+            "Plack::Middleware::Debug" : "0",
+            "Router::Simple::Declare" : "0",
+            "Starman" : "0",
+            "String::ShellQuote" : "0",
+            "Zonemaster" : "0",
+            "perl" : "5.014002"
+         }
+      }
+   },
+   "release_status" : "stable",
+   "resources" : {
+      "license" : [
+         "http://dev.perl.org/licenses/"
+      ]
+   },
+   "version" : "1.1.0"
+}
diff --git a/MYMETA.yml b/MYMETA.yml
new file mode 100644
index 0000000..781cdc6
--- /dev/null
+++ b/MYMETA.yml
@@ -0,0 +1,53 @@
+---
+abstract: 'The Zonemaster project Web Backend engine'
+author:
+  - 'Michal TOMA <toma at nic.fr>'
+build_requires:
+  DBD::SQLite: '0'
+  ExtUtils::MakeMaker: '6.59'
+configure_requires:
+  ExtUtils::MakeMaker: '0'
+dynamic_config: 0
+generated_by: 'Module::Install version 1.14, CPAN::Meta::Converter version 2.143240'
+license: perl
+meta-spec:
+  url: http://module-build.sourceforge.net/META-spec-v1.4.html
+  version: '1.4'
+name: Zonemaster-WebBackend
+no_index:
+  directory:
+    - CodeSnippets
+    - inc
+    - share
+    - t
+recommends:
+  DBD::Pg: '0'
+  DBD::SQLite: '0'
+  DBD::mysql: '0'
+requires:
+  Config::IniFiles: '0'
+  DBI: '1.616'
+  Daemon::Control: '0'
+  File::ShareDir: '0'
+  File::Slurp: '0'
+  HTML::Entities: '0'
+  IO::CaptureOutput: '0'
+  JSON::PP: '0'
+  JSON::RPC::Dispatch: '0'
+  LWP::UserAgent: '0'
+  Locale::TextDomain: '0'
+  Moose: '2.04'
+  Moose::Role: '0'
+  Net::IP::XS: '0.14'
+  Net::LDNS: '0.72'
+  Parallel::ForkManager: '1.12'
+  Plack::Builder: '0'
+  Plack::Middleware::Debug: '0'
+  Router::Simple::Declare: '0'
+  Starman: '0'
+  String::ShellQuote: '0'
+  Zonemaster: '0'
+  perl: '5.014002'
+resources:
+  license: http://dev.perl.org/licenses/
+version: 1.1.0
diff --git a/README.md b/README.md
index 608eb1a..4f6aa8b 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-Zonemaster
-==========
+Zonemaster Backend
+==================
 [![Build Status](https://travis-ci.org/dotse/zonemaster-backend.svg?branch=master)](https://travis-ci.org/dotse/zonemaster-backend)
 
 ### Purpose
@@ -19,10 +19,19 @@ analysing.
 
 ### Prerequisites
 
-Before you install the Zonemaster CLI utility, you need the
-Zonemaster Engine test framework installed. Please see the
+Before you install the Zonemaster Backend, you need the
+Zonemaster Engine installed. Please see the
 [Zonemaster Engine installation
-instructions](https://github.com/dotse/zonemaster-engine/blob/master/docs/installation.md)
+instructions](https://github.com/dotse/zonemaster-engine/blob/master/docs/installation.md).
+
+### Upgrade 
+
+If you are upgrading Zonemaster Backend from 1.0.X to 1.1.X please follow the
+[upgrade instructions from 1.0.X to 1.1.X](docs/upgrade-from-1.0.x-to-1.1.x.md) and then follow the
+relevant parts of the installation instructions below.
+
+For all other upgrades follow the relevant parts of the installation
+instructions below.
 
 ### Installation
 
@@ -30,19 +39,35 @@ Follow the detailed [installation instructions](docs/installation.md).
 
 ### Configuration 
 
-Text for configuring the backend are found in the [installation
-instructions](docs/installation.md).
+Zonemaster *Backend* is configured as a whole from `/etc/zonemaster/backend_config.ini`.
+
+>
+> At this time there is no documentation for `backend_config.ini`.
+>
+
 
 ### Documentation
 
-There is a fully documented [API](docs/API.md), which is the primay way
-to use the backend. The [docs](docs/) directory also contains the SQL commands
-for manipulating the database. 
+The Zonemaster Backend documentation is split up into several documents:
+
+* A number of [Typographic Conventions](docs/TypographicConventions.md) are used
+  throughout this documentation.
+* The [Architecture](docs/Architecture.md) document describes each of the
+  Zonemaster Backend components and how they operate. It also discusses all
+  central concepts needed to understand the Zonemaster backend, and contains a
+  glossary over domain specific technical terms.
+* The [Getting Started](docs/GettingStarted.md) guide walks you through creating
+  a *test* and following it through its life cycle, all using JSON-RPC calls to
+  the *Web backend*.
+* The [API](docs/API.md) documentation describes the *Web backend* inteface in
+  detail.
+
+The [docs](docs/) directory also contains the SQL commands for manipulating the
+database. 
+
 
 License
 =======
 
 The software is released under the 2-clause BSD license. See separate
 [LICENSE](LICENSE) file.
-
-
diff --git a/docs/API.md b/docs/API.md
index d4f4a8e..707eeec 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -1,578 +1,1109 @@
-# Introduction
+# API
 
-This document describes the API of the Zonemaster Backend.
+## Purpose
 
-The API is available in the JSON-RPC (version 2.0) format.
+This document describes the JSON-RPC API provided by the Zonemaster *Web backend*.
+This API provides means to check the health of domains and to fetch domain health reports.
+Health checks are called *tests* in Zonemaster lingo.
 
-Many libraries in about all languages are available to communicate using
-the JSON-RPC protocol.
 
-## Backend API
+## Protocol
 
-### JSON-RPC Call 1: version\_info
-This API returns the version of the Backend+Engine software combination. It is the simplest API to use to check that the backend is running and abswering properly.
+This API is implemented using [JSON-RPC 2.0](http://www.jsonrpc.org/specification).
 
-**Request**:
-```
+JSON-RPC request objects are accepted in the body of HTTP POST requests to any path.
+The HTTP request must contain the header `Content-Type: application/json`.
+
+All JSON-RPC request and response objects have the keys `"jsonrpc"`, `"id"` and `"method"`.
+For details on these, refer to the JSON-RPC 2.0 specification.
+
+
+### Deviations from JSON-RPC 2.0
+
+* The `"jsonrpc"` property is not checked.
+* The error code -32603 is used for invalid arguments, as opposed to the dedicated error code -32602.
+* When standard error codes are used, the accompanying messages are not the standard ones.
+
+
+### Notes on the JSON-RPC 2.0 implementation
+
+* Extra top-level properties in request objects are allowed but ignored.
+* Extra properties in the `"params"` object are allowed for some methods but ignored for others.
+* Error messages from the API should be considered sensitive as they sometimes leak details about the internals of the application and the system.
+* The error code -32601 is used when the `"method"` property is missing, rather than the perhaps expected error code -32600.
+
+
+## Request handling
+
+When a method expects a string argument but receives an array or an object,
+the value may be interpreted as something like `"ARRAY(0x1a2d1d0)"` or `"HASH(0x1a2d2c8)"`.
+
+When a method expects a boolean argument, any kind of value is accepted.
+A number of values are interpreted as false: `false`, `null`, `""`, `"0"` and any number equal to zero.
+Everything else is interpreted as true.
+
+When a method expects an integer arguments, numbers encoded in strings are also accepted and used transparently,
+and numbers with fractions are rounded to the nearest integer.
+
+For details on when a *test* are performed after it's been requested,
+see the [architecture documentation](Architecture.md).
+
+
+## Error reporting
+
+If the request object is invalid JSON, an error with code `-32700` is reported.
+
+If no method is specified or an invalid method is specified, an error with code `-32601` is reported.
+
+All error states that occur after the RPC method has been identified are reported as internal errors with code `-32603`.
+
+
+## Privilege levels
+
+This API provides three classes of methods:
+
+* *Unrestricted* methods are available to anyone with access to the API.
+* *Authenticated* methods have parameters for username and API key credentials.
+* *Administrative* methods require that the connection to the API is opened from localhost (`127.0.0.1` or `::1`).
+
+
+## Data types
+
+This sections describes a number of data types used in this API. Each data type
+is based on a JSON data type, but additionally imposes its own restrictions.
+
+
+### Batch id
+
+Basic data type: number
+
+An positive integer.
+
+The unique id of a *batch*.
+
+
+### Domain name
+
+Basic data type: string
+
+1. If the string is a single character, that character must be `.`.
+
+2. The length of the string must not be greater than 254 characters.
+
+3. When the string is split at `.` characters (after IDNA conversion,
+   if needed), each component part must be at most 63 characters long.
+
+> Note: Currently there are no restrictions on what characters that are allowed.
+
+
+### DS info
+
+Basic data type: object
+
+Properties:
+
+* `"digest"`: A string, required. Either 40 or 64 hexadecimal characters (case insensitive).
+* `"algorithm"`: An integer, optional.
+* `"digtype"`: An integer, optional.
+* `"keytag"`: An integer, optional.
+
+Extra properties in *DS info* objects are ignored when present in RPC method arguments, and never returned as part of RPC method results.
+
+
+### Name server
+
+Basic data type: object
+
+Properties:
+
+* `"ns"`: A *domain name*, required.
+* `"ip"`: An IPv4 or IPv6 address, required.
+
+
+### Priority
+
+Basic data type: number
+
+This parameter is any integer that will be used by The Zonemaster Backend Worker daemon to sort the test requests from highest to lowest priority.
+This parameter will typically be used in a setup where a GUI will send requests to the Backend API and would like to get response as soon as possible while at the same time using the idle time for background batch testing.
+The drawback of this setup will be that the GUI will have to wait for at least one background processing slot to become free (would be a few secods in a typical installation with up to 30 parallel zonemaster processes allowed)
+
+### Queue
+
+Basic data type: number
+
+This parameter allows an optional separation of testing in the same database. The default value for the queue is 0. It is closely related to the *lock_on_queue* parameter of the [ZONEMASTER] section of the backend_config.ini file.
+The typical use case for this parameter would be a setup with several separate Backends Worker daemons running on separate physical or virtual machines each one dedicated to a specific task, for example queue 0 for frontend tests and queue 1 dedicated to batch testing. Running several Backend Worker daemons on the same machine is currently not supported.
+
+
+### Profile name
+
+Basic data type: string
+
+The name of a [*profile*](Architecture.md#profile).
+
+One of the strings:
+
+* `"default_profile"`
+* `"test_profile_1"`
+* `"test_profile_2"`
+
+The `"test_profile_2"` *profile* is identical to `"default_profile"`.
+
+>
+> TODO: What is the expected behavior when a *profile* other than the ones listed above is requested?
+>
+
+
+### Progress percentage
+
+Basic data type: number
+
+An integer ranging from 0 (not started) to 100 (finished).
+
+
+### Severity level
+
+Basic data type: string
+
+One of the strings (in order from least to most severe):
+
+* `"DEBUG"`
+* `"INFO"`
+* `"NOTICE"`
+* `"WARNING"`
+* `"ERROR"`
+* `"CRITICAL"`
+
+
+### Test id
+
+Basic data type: string
+
+Each *test* has a unique *test id*.
+
+
+### Test result
+
+Basic data type: object
+
+The object has three keys, `"module"`, `"message"` and `"level"`.
+
+* `"module"`: a string. The *test module* that produced the result.
+* `"message"`: a string. A human-readable *message* describing that particular result.
+* `"level"`: a *severity level*. The severity of the message.
+
+Sometimes additional keys are present.
+
+* `"ns"`: a *domain name*. The name server used by the *test module*.
+
+>
+> TODO: Can other extra keys in addition to `"ns"` occur here? Can something be said
+> about when each extra key is present?
+>
+
+
+### Timestamp
+
+Basic data type: string
+
+>
+> TODO: Specify date format
+>
+
+
+### Translation language
+
+Basic data type: string
+
+* Any string starting with `"fr"` is interpreted as French.
+* Any string starting with `"sv"` is interpreted as Swedish.
+* Any other string is interpreted as English.
+
+
+## API method: `version_info`
+
+Returns the version of the *Backend*+*Engine* software combination.
+
+Example request:
+```json
 {
-   "params" : "version_info",
-   "jsonrpc" : "2.0",
-   "id" : 143014362197299,
-   "method" : "version_info"
+  "jsonrpc": "2.0",
+  "id": 1,
+  "method": "version_info"
 }
 ```
 
- -  params: any non empty parameter (empty parameters are not supported as of now)
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  method: the name of the called method
-
-**Response**:
-```
+Example response:
+```json
 {
-   "jsonrpc" : "2.0",
-   "id" : 143014362197299,
-   "result" : "Zonemaster Test Engine Version: v1.0.3"
+  "jsonrpc": "2.0",
+  "id": 1,
+  "result": {
+    "zonemaster_backend": "1.0.7",
+    "zonemaster_engine": "v1.0.14"
+  }
 }
 ```
 
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  result: the version string
 
-### JSON-RPC Call 2: get\_ns\_ips
-This API id used by the NS/IP input forms of the "Undelegated domain test tab". Given a nameserver it returns all of its IP addresses.
+#### `"result"`
+
+An object with the following properties:
+
+* `"zonemaster_backend"`: A string. The version number of the running *Web backend*.
+* `"zonemaster_engine"`: A string. The version number of the *Engine* used by the *Web backend*.
+
 
-**Request**:
+#### `"error"`
+
+>
+> TODO: List all possible error codes and describe what they mean enough for clients to know how react to them.
+>
+
+
+## API method: `get_ns_ips`
+
+Looks up the A and AAAA records for a *domain name* on the public Internet.
+
+Example request:
+```json
+{
+  "jsonrpc": "2.0",
+  "id": 2,
+  "method": "get_ns_ips",
+  "params": "zonemaster.net"
+}
 ```
+
+Example response:
+```json
 {
-   "params" : "ns1.nic.fr",
-   "jsonrpc" : "2.0",
-   "id" : 143014382480608,
-   "method" : "get_ns_ips"
+  "jsonrpc": "2.0",
+  "id": 2,
+  "result": [
+    {
+      "zonemaster.net": "192.134.4.83"
+    },
+    {
+      "zonemaster.net": "2001:67c:2218:3::1:83"
+    }
+  ]
 }
 ```
 
- -  params: the name of the server whose IPs need to be resolved
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  method: the name of the called method
 
-**Response**:
+#### `"params"`
+
+A *domain name*. The *domain name* whose IP addresses are to be resolved.
+
+
+#### `"result"`
+
+A list of one or two objects representing IP addresses (if 2 one is for IPv4 the
+other for IPv6). The objects each have a single key and value. The key is the
+*domain name* given as input. The value is an IP address for the name, or the
+value `0.0.0.0` if the lookup returned no A or AAAA records.
+
+>
+> TODO: If the name resolves to two or more IPv4 address, how is that represented?
+>
+
+#### `"error"`
+
+>
+> TODO: List all possible error codes and describe what they mean enough for clients to know how react to them.
+>
+
+
+## API method: `get_data_from_parent_zone`
+
+Returns all the NS/IP and DS/DNSKEY/ALGORITHM pairs of the domain from the
+parent zone.
+
+Example request:
+```json
+{
+  "jsonrpc": "2.0",
+  "id": 3,
+  "method": "get_data_from_parent_zone",
+  "params": "zonemaster.net"
+}
 ```
+
+Example response:
+```json
 {
-   "jsonrpc" : "2.0",
-   "id" : 143014382480608,
-   "result" : [
+  "jsonrpc": "2.0",
+  "id": 3,
+  "result": {
+    "ns_list": [
+      {
+        "ns": "ns.nic.se",
+        "ip": "2001:67c:124c:100a::45"
+      },
       {
-         "ns1.nic.fr" : "192.134.4.1"
+        "ns": "ns.nic.se",
+        "ip": "91.226.36.45"
       },
+      ...
+    ],
+    "ds_list": [
       {
-         "ns1.nic.fr" : "2001:660:3003:2::4:1"
+        "algorithm": 5,
+        "digtype": 2,
+        "keytag": 54636,
+        "digest": "cb496a0dcc2dff88c6445b9aafae2c6b46037d6d144e43def9e68ab429c01ac6"
+      },
+      {
+        "keytag": 54636,
+        "digest": "fd15b55e0d8ee2b5a8d510ab2b0a95e68a78bd4a",
+        "algorithm": 5,
+        "digtype": 1
       }
-   ]
+    ]
+  }
 }
 ```
 
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  result: a list of one or two IP addresses (if 2 one is for IPv4 the
-    other for IPv6)
+>
+> Note: The above example response was abbreviated for brevity to only include
+> the first two elments in each list. Omitted elements are denoted by a `...`
+> symbol.
+>
 
-### *JSON-RPC Call 3*: get\_data\_from\_parent\_zone
-This API returns all the NS/IP and DS/DNSKEY/ALGORITHM pairs of the domain from the parent zone. It is used by the "Fetch data from parent zone" button of the "Undelegated domain test" tab of the web interface.
 
-**Request**:
-```
+#### `"params"`
+
+A *domain name*. The domain whose DNS records are requested.
+
+
+#### `"result"`
+
+An object with the following properties:
+
+* `"ns_list"`: A list of *name server* objects representing the nameservers of the given *domain name*.
+* `"ds_list"`: A list of *DS info* objects.
+
+
+>
+> TODO: Add wording about what the `"ds_list"` objects represent.
+>
+
+
+#### `"error"`
+
+>
+> TODO: List all possible error codes and describe what they mean enough for clients to know how react to them.
+>
+
+
+## API method: `start_domain_test`
+
+Enqueues a new *test*.
+
+If an identical *test* was already enqueued and hasn't been started or was enqueued less than 10 minutes earlier,
+no new *test* is enqueued.
+Instead the id for the already enqueued or run test is returned.
+
+*Tests* enqueud using this method are assigned a *priority* of 10.
+
+Example request:
+```json
 {
-   "params" : "nic.fr",
-   "jsonrpc" : "2.0",
-   "id" : 143014391379310,
-   "method" : "get_data_from_parent_zone"
+  "jsonrpc": "2.0",
+  "id": 4,
+  "method": "start_domain_test",
+  "params": {
+    "client_id": "Zonemaster Dancer Frontend",
+    "domain": "zonemaster.net",
+    "profile": "default_profile",
+    "client_version": "1.0.1",
+    "nameservers": [
+      {
+        "ip": "2001:67c:124c:2007::45",
+        "ns": "ns3.nic.se"
+      },
+      {
+        "ip": "192.93.0.4",
+        "ns": "ns2.nic.fr"
+      }
+    ],
+    "ds_info": [],
+    "advanced": true,
+    "ipv6": true,
+    "ipv4": true
+  }
 }
 ```
 
- -  params: the domain name currently being tested
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  method: the name of the called method
-
-**Response**:
-```
+Example response:
+```json
 {
-   "jsonrpc" : "2.0",
-   "id" : 143014391379310,
-   "result" : {
-      "ds_list" : [
-         {
-            "algorithm" : "sha256",
-            "digest" : "84103c835179a682c25c9647d8c962ab183eb44c80e12e9542c4ae32a2e80b76",
-            "keytag" : 11627
-         }
-      ],
-      "ns_list" : [
-         {
-            "ns" : "ns6.ext.nic.fr.",
-            "ip" : "130.59.138.49"
-         },
-         {
-            "ns" : "ns6.ext.nic.fr.",
-            "ip" : "2001:620:0:1b:5054:ff:fe74:8780"
-         },
-         {
-            "ns" : "ns3.nic.fr.",
-            "ip" : "192.134.0.49"
-         },
-         {
-            "ns" : "ns3.nic.fr.",
-            "ip" : "2001:660:3006:1::1:1"
-         },
-         {
-            "ns" : "ns2.nic.fr.",
-            "ip" : "192.93.0.4"
-         },
-         {
-            "ns" : "ns2.nic.fr.",
-            "ip" : "2001:660:3005:1::1:2"
-         },
-         {
-            "ns" : "ns1.ext.nic.fr.",
-            "ip" : "193.51.208.13"
-         },
-         {
-            "ns" : "ns4.ext.nic.fr.",
-            "ip" : "193.0.9.4"
-         },
-         {
-            "ns" : "ns4.ext.nic.fr.",
-            "ip" : "2001:67c:e0::4"
-         },
-         {
-            "ns" : "ns1.nic.fr.",
-            "ip" : "192.134.4.1"
-         },
-         {
-            "ns" : "ns1.nic.fr.",
-            "ip" : "2001:660:3003:2::4:1"
-         }
-      ]
-   }
+  "jsonrpc": "2.0",
+  "id": 4,
+  "result": "c45a3f8256c4a155"
 }
 ```
 
- -   jsonrpc: « 2.0 »
- -   id: any kind of unique id allowing to match requests and responses
- -   result: a list of several { nameserver =\> IP\_adress } pairs, and a list of DS information objects.
 
-### *JSON-RPC Call 4*: validate\_syntax
-This API checks the "params" structure for syntax coherence. It is very strict on what is allowed and what is not to avoid any SQL injection and cross site scripting attempts. It also checks the domain name for syntax to ensure the domain name seems to be a valid domain name and a test by the Engine can be started.
+#### `"params"`
 
-**Request**:
-```
+An object with the following properties:
+
+* `"client_id"`: A free-form string, optional.
+* `"domain"`: A *domain name*, required.
+* `"profile"`: A *profile name*, optional.
+* `"client_version"`: A free-form string, optional.
+* `"nameservers"`: A list of *name server* objects, optional.
+* `"ds_info"`: A list of *DS info* objects, optional.
+* `"advanced"`: **Deprecated**. A boolean, optional.
+* `"ipv6"`: A boolean, optional. (default `false`)
+* `"ipv4"`: A boolean, optional. (default `false`)
+* `"config"`: A string, optional. The name of a *config profile*.
+* `"user_ip"`: A ..., optional.
+* `"user_location_info"`: A ..., optional.
+* `"priority"`: A *priority*, optional
+* `"queue"`: A *queue*, optional
+
+>
+> TODO: Clarify the data type of the following `"params"` properties:
+> `"user_ip"` and `"user_location_info"`.
+>
+> TODO: Clarify the purpose of each `"params"` property.
+>
+> TODO: Clarify the default value of each optional `"params"` property.
+>
+
+
+#### `"result"`
+
+A *test id*. The newly started *test*, or a recently run *test* with the same
+parameters.
+started within the recent configurable short time.
+
+>
+> TODO: Specify which configuration option controls the duration of the window
+> of *test* reuse.
+>
+
+
+#### `"error"`
+
+>
+> TODO: List all possible error codes and describe what they mean enough for clients to know how react to them.
+>
+
+
+## API method: `test_progress`
+
+Reports on the progress of a *test*.
+
+Example request:
+```json
 {
-   "params" : {
-      "domain" : "afnic.fr",
-      "ipv6" : 1,
-      "ipv4" : 1,
-      "nameservers" : [
-         {
-            "ns" : "ns1.nic.fr",
-            "ip" : "1.2.3.4"
-         },
-         {
-            "ns" : "ns2.nic.fr",
-            "ip" : "192.134.4.1"
-         }
-      ]
-   },
-   "jsonrpc" : "2.0",
-   "id" : 143014426992009,
-   "method" : "validate_syntax"
+  "jsonrpc": "2.0",
+  "id": 5,
+  "method": "test_progress",
+  "params": "c45a3f8256c4a155"
 }
 ```
- -  params: the structure representing the frontend parameters structure (see the start_domain_test API for a detailed description)
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  method: the name of the called method
 
-**Response**:
-```
+Example response:
+```json
 {
-   "jsonrpc" : "2.0",
-   "id" : 143014426992009,
-   "result" : {
-      "status" : "ok",
-      "message" : "Syntax ok"
-   }
+  "jsonrpc": "2.0",
+  "id": 5,
+  "result": 100
 }
 ```
 
- -   jsonrpc: « 2.0 »
- -   id : any kind of unique id allowing to match requests and responses
- -   result: either “syntax\_ok” or “syntax\_not\_ok”.
 
-### *JSON-RPC Call 5*: start\_domain\_test
-This API inserts a new test request into the database. The test request is inserted with a "progress" (one of the database fields) value of 0 meaning the Engine can start testing this domain.
-The testing is done by a (typically) cron job on the backend machine.
+#### `"params"`
+
+A *test id*. The *test* to report on.
+
+
+#### `"result"`
+
+A *progress percentage*.
+
 
-**Request**:
+#### `"error"`
+
+>
+> TODO: List all possible error codes and describe what they mean enough for clients to know how react to them.
+>
+
+
+## API method: `get_test_results`
+
+Return all *test result* objects of a *test*, with *messages* in the requested *translation language*.
+
+Example request:
+```json
+{
+  "jsonrpc": "2.0",
+  "id": 6,
+  "method": "get_test_results",
+  "params": {
+    "id": "c45a3f8256c4a155",
+    "language": "en"
+  }
+}
 ```
+
+Example response:
+```json
 {
-   "jsonrpc" : "2.0",
-   "method" : "start_domain_test",
-   "params" : {
-      "client_id" : "Zonemaster Dancer Frontend",
-      "domain" : "afnic.FR",
-      "profile" : "default_profile",
-      "client_version" : "1.0.1",
-      "nameservers" : [
-         {
-            "ip" : "192.134.4.1",
-            "ns" : "ns1.nic.FR."
-         },
-         {
-            "ip" : "2001:660:3003:2:0:0:4:1",
-            "ns" : "ns1.nic.FR."
-         },
-         {
-            "ip" : "192.134.0.49",
-            "ns" : "ns3.nic.FR."
-         },
-         {
-            "ns" : "ns3.nic.FR.",
-            "ip" : "2001:660:3006:1:0:0:1:1"
-         },
-         {
-            "ns" : "ns2.nic.FR.",
-            "ip" : "192.93.0.4"
-         },
-         {
-            "ns" : "ns2.nic.FR.",
-            "ip" : "2001:660:3005:1:0:0:1:2"
-         }
+  "jsonrpc": "2.0",
+  "id": 6,
+  "result": {
+    "creation_time": "2016-11-15 11:53:13.965982",
+    "id": 25,
+    "hash_id": "c45a3f8256c4a155",
+    "params": {
+      "ds_info": [],
+      "client_version": "1.0.1",
+      "domain": "zonemaster.net",
+      "profile": "default_profile",
+      "ipv6": true,
+      "advanced": true,
+      "nameservers": [
+        {
+          "ns": "ns3.nic.se",
+          "ip": "2001:67c:124c:2007::45"
+        },
+        {
+          "ip": "192.93.0.4",
+          "ns": "ns2.nic.fr"
+        }
       ],
-      "ds_info" : [],
-      "advanced" : true,
-      "ipv6" : true,
-      "ipv4" : true
-   },
-   "id" : 143014514892268
+      "ipv4": true,
+      "client_id": "Zonemaster Dancer Frontend"
+    },
+    "results": [
+      {
+        "module": "SYSTEM",
+        "message": "Using version v1.0.14 of the Zonemaster engine.\n",
+        "level": "INFO"
+      },
+      {
+        "message": "Configuration was read from DEFAULT CONFIGURATION\n",
+        "level": "INFO",
+        "module": "SYSTEM"
+      },
+      ...
+    ]
+  }
 }
 ```
 
--   params:
-    -   client\_id: "Zonemaster CGI/Dancer/node.js",
-        -   \# free string
-    -   client\_version: "1.0",
-        -   \# free version like string
-    -   domain: "afnic.FR",
-        -   \# content of the domain text field
-    -   advanced: true,
-        -   \# true or false, if the advanced options checkbox checked
-    -   ipv4: true,
-        -   \# true or false, is the ipv4 checkbox checked
-    -   ipv6: true,
-        -   \# true or false, is the ipv6 checkbox checked
-    -   profile: 'default\_profile\_1',
-        -   \# the id of the Test profile listbox
-    -   nameservers: [
-        -   \# list of the namaserves up to 32
-            - {
-              "ns" : "ns2.nic.FR.",
-              "ip" : "192.93.0.4",
-            },
-            - {
-              "ns" : "ns2.nic.FR.",
-              "ip" : "2001:660:3005:1:0:0:1:2",
-            }
-    -   ds\_digest\_pairs: []
-        - disabled in the present version
+>
+> Note: The above example response was abbreviated for brevity to only include
+> the first two elments in each list. Omitted elements are denoted by a `...`
+> symbol.
+>
+
+
+#### `"params"`
+
+An object with the following properties:
+
+* `"id"`: A *test id*, required.
+* `"language"`: A *translation language*, required.
+
+
+#### `"result"`
+
+An object with a the following properties:
+
+* `"creation_time"`: A *timestamp*. The time at which the *test* was enqueued.
+* `"id"`: An integer.
+* `"hash_id"`: A string.
+* `"params"`: The `"params"` object sent to `start_domain_test` when the *test*
+  was started.
+* `"results"`: A list of *test result* objects.
+
+>
+> TODO: Specify the MD5 hash format.
+>
+> TODO: What about if the Test was created with `add_batch_job` or something
+> else?
+>
+> TODO: It's confusing that the method is named `"start_domain_test"`, when
+> it doesn't actually start the *test*.
+>
+
+
 
- -   jsonrpc: « 2.0 »
- -   id: any kind of unique id allowing to match requests and responses
- -   method: the name of the called method
+#### `"error"`
 
-**Response**:
+>
+> TODO: List all possible error codes and describe what they mean enough for clients to know how react to them.
+>
+
+
+## API method: `get_test_history`
+
+Returns a list of completed *tests* for a domain.
+
+Example request:
+```json
+{
+  "jsonrpc": "2.0",
+  "id": 7,
+  "method": "get_test_history",
+  "params": {
+    "offset": 0,
+    "limit": 200,
+    "frontend_params": {
+      "client_id": "Zonemaster Dancer Frontend",
+      "domain": "zonemaster.net",
+      "profile": "default_profile",
+      "client_version": "1.0.1",
+      "nameservers": [
+        {
+          "ns": "ns3.nic.se",
+          "ip": "2001:67c:124c:2007::45"
+        },
+        {
+          "ns": "ns2.nic.fr",
+          "ip": "192.93.0.4"
+        }
+      ],
+      "ds_info": [],
+      "advanced": true,
+      "ipv6": true,
+      "ipv4": true
+    }
+  }
+}
 ```
+
+Example response:
+```json
 {
-   "id" : 143014514892268,
-   "jsonrpc" : "2.0",
-   "result" : 8881
+  "id": 7,
+  "jsonrpc": "2.0",
+  "result": [
+    {
+      "id": "c45a3f8256c4a155",
+      "creation_time": "2016-11-15 11:53:13.965982",
+      "overall_result": "error",
+      "advanced_options": null
+    },
+    {
+      "id": "32dd4bc0582b6bf9",
+      "creation_time": "2016-11-14 08:46:41.532047",
+      "overall_result": "error",
+      "advanced_options": null
+    },
+    ...
+  ]
 }
 ```
 
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  result: the id of the test\_result (this id will be used in the
-    other APIs related to the same test result).
+>
+> Note: The above example response was abbreviated for brevity to only include
+> the first two elments in each list. Omitted elements are denoted by a `...`
+> symbol.
+>
 
-### *JSON-RPC Call 6*: test\_progress
-This API returns the value of the "progress" parameter from the database. Once the progress reaches 100 the test is finished and the results may be retrieved for display.
 
-**Request**:
-```
+#### `"params"`
+
+An object with the following properties:
+
+* `"offset"`: An integer, optional. (default: 0).
+* `"limit"`: An integer, optional. (default: 200).
+* `"frontend_params"`: As described below.
+
+The value of `"frontend_params"` is an object in turn, with the
+keys `"domain"` and `"nameservers"`. `"domain"` and `"nameservers"`
+will be used to look up all tests for the given domain, separated
+according to if they were started with a `"nameservers"` parameter or
+not.
+
+>
+> TODO: Do we have an SQL injection opportunity here?
+>
+> TODO: Describe the remaining keys in the example
+>
+> TODO: Describe the purpose of `"offset"` and `"limit"`
+>
+> TODO: Is the `"nameservers"` value a boolean in disguise?
+>
+> TODO: The description of `"frontend_params"` is clearly not up to date. Can it
+> be described in a better way?
+>
+
+
+#### `"result"`
+
+An object with the following properties:
+
+* `"id"` A *test id*.
+* `"creation_time"`: A *timestamp*. Time when the Test was enqueued.
+* `"advanced_options"`: **Deprecated**. A string or `null`.
+  `"1"` if the `"advanced"` flag was set in the method call to `start_domain_test` that created this Test.
+  In some future release this property will no longer be included in the result.
+* `"overall_result"`: A string. The most severe problem level logged in the test results.
+
+>
+> TODO: Describe the format of `"overall_result"`.
+>
+> TODO: What about if the *test* was created with `add_batch_job` or something else?
+>
+> TODO: What about if the *test* was created with `"advanced"` set to `false` in `start_domain_test`?
+>
+
+
+#### `"error"`
+
+>
+> TODO: List all possible error codes and describe what they mean enough for clients to know how react to them.
+>
+
+
+## API method: `add_api_user`
+
+>
+> TODO: Method description.
+>
+
+This method requires the *administrative* *privilege level*.
+
+Example request:
+```json
 {
-   "method" : "test_progress",
-   "jsonrpc" : "2.0",
-   "id" : 143014514915128,
-   "params" : "8881"
+  "jsonrpc": "2.0",
+  "method": "add_api_user",
+  "id": 4711,
+  "params": {
+    "username": "citron",
+    "api_key": "fromage"
+  }
 }
 ```
 
- -  params: the id of the test whose progress indicator has to be
-    determined.
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  method: the name of the called method
-
-**Response**:
-```
+Example response:
+```json
 {
-   "jsonrpc" : "2.0",
-   "result" : 0,
-   "id" : 143014514915128
+  "id": 4711,
+  "jsonrpc": "2.0",
+  "result": 0
 }
 ```
 
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  result: the % of completion of the test from 0% to 100%
 
-### *JSON-RPC Call 7*: get\_test\_results
-This API returns the test result JSON structure from the database. The test results are stored in a language independent format in the database. They are translated into the language given in the "language" parameter and returned to the caller of this API.
+#### `"params"`
 
-**Request**:
-```
+An object with the following properties:
+
+* `"username"`: A string, optional. The name of the user to add.
+* `"api_key"`: A string, optional. The API key (in effect, password) for the user to add.
+
+>
+> TODO: Are `"username"` and `"api_key"` really supposed to be optional? Because
+> they are now, is that a bug? I get `"result": 0` when I omit them. I would
+> have expected parameter validation errors.
+>
+
+
+#### `"result"`
+
+An integer.
+
+>
+> TODO: Describe the possible values of the result and what they mean.
+>
+
+
+#### `"error"`
+
+>
+> TODO: List all possible error codes and describe what they mean enough for clients to know how react to them.
+>
+
+
+## API method: `add_batch_job`
+
+>
+> TODO: Method description.
+>
+
+All the domains will be tested using identical parameters.
+
+An *api user* can only have one un-finished *batch* at a time.
+
+If an identical *test* for a domain was already enqueued and hasn't been started or was enqueued less than 10 minutes earlier,
+no new *test* is enqueued for this domain.
+
+*Tests* enqueud using this method are assigned a *priority* of 5.
+
+
+Example request:
+```json
 {
-   "id" : 143014516614517,
-   "params" : {
-      "language" : "en",
-      "id" : "8881"
-   },
-   "jsonrpc" : "2.0",
-   "method" : "get_test_results"
+  "jsonrpc": "2.0",
+  "id": 147559211348450,
+  "method": "add_batch_job",
+  "params" : {
+    "api_key": "fromage",
+    "username": "citron",
+    "test_params": {},
+    "domains": [
+      "zonemaster.net",
+      "domain1.se",
+      "domain2.fr"
+    ]
+  }
 }
 ```
 
- -  params:
-     -  id: the id of the test whose results we want to get.
-     -  language: the language of the user interface
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  method: the name of the called method
-
-**Response**:
-```
-{
-  "jsonrpc" : "2.0",
-  "id" : 140723510525000,
-  "result" : {
-    "params" : {
-.
-.
-TEST PARAMS (See *JSON-RPC Call 5*: start_domain_test)
-.
-.
-  },
-  "id": 8881,
-  "creation_time": "2014-08-05 12:00:13.401442",
-  "results": [
-    {
-      "module": 'DELEGATION',
-      "message": 'Messsage for DELEGATION/NAMES_MATCH in the language:fr'
-      "level": 'NOTICE',
-    },
-.
-.
-LIST OF TEST RESULTS
-.
+Example response:
+```json
 {
-  "ns": "ns1.nic.fr",
-  "module": "NAMESERVER",
-  "message": "Messsage for NAMESERVER/AXFR_FAILURE in the language:fr"
-  "level": "NOTICE",
-},
-.
-.
-LIST OF TEST RESULTS
-.
-.
-]
-}
+    "jsonrpc": "2.0",
+    "id": 147559211348450,
+    "result": 8
 }
 ```
 
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  result: Contains:
-     -  id: The id of the test whose results are returnes
-     -  creation\_time: The exact time the test was created
-     -  params: The parameters used to run this test (See *JSON-RPC Call
-        5*: start\_domain\_test)
-     -  results: A list of results.
 
-#### Description of the results:
+#### `"params"`
 
-The individual results are of the form
+An object with the following properties:
 
-```
+* `"username"`: A string. The username of this batch.
+* `"api_key"`: A string. The api_key associated with the username username of this *batch*.
+* `"domains"`: A list of *domain names*. The domains to be tested.
+* `"test_params"`: As described below.
+
+The value of `"test_params"` is an object with the following properties:
+
+* `"client_id"`: A free-form string, optional.
+* `"profile"`: A *profile name*, optional.
+* `"client_version"`: A free-form string, optional.
+* `"nameservers"`: A list of *name server* objects, optional.
+* `"ds_info"`: A list of *DS info* objects, optional.
+* `"advanced"`: **Deprecated**. A boolean, optional.
+* `"ipv6"`: A boolean, optional. (default: `false`)
+* `"ipv4"`: A boolean, optional. (default: `false`)
+* `"config"`: A string, optional. The name of a *config profile*.
+* `"user_ip"`: A ..., optional.
+* `"user_location_info"`: A ..., optional.
+* `"priority"`: A *priorty*, optional
+* `"queue"`: A *queue*, optional
+
+
+>
+> TODO: Clarify the data type of the following `"frontend_params"` properties:
+> `"user_ip"` and `"user_location_info"`.
+>
+> TODO: Clarify which `"params"` and `"frontend_params"` properties are optional
+> and which are required.
+>
+> TODO: Clarify the default value of each optional `"params"` and
+> `"frontend_params"` property.
+>
+> TODO: Clarify the purpose of each `"params"` and `"frontend_params"` property.
+>
+> TODO: Are domain names actually validated in practice?
+>
+
+
+#### `"result"`
+
+A *batch id*.
+
+
+#### `"error"`
+
+* You can't create a new batch job.
+  A *batch* with unfinished *tests* already exists for this *api user*.
+
+>
+> TODO: List all possible error codes and describe what they mean enough for clients to know how react to them.
+>
+
+
+## API method: `get_batch_job_result`
+
+>
+> TODO: Method description.
+>
+
+Example request:
+```json
 {
-  "module": "DELEGATION",
-  "message": "Messsage for DELEGATION/NAMES_MATCH in the language:fr"
-  "level": "NOTICE",
+    "jsonrpc": "2.0",
+    "id": 147559211994909,
+    "method": "get_batch_job_result",
+    "params": "8"
 }
 ```
 
-Or
-
-```
+Example response:
+```json
 {
-  "ns": "ns1.nic.fr",
-  "module": "NAMESERVER",
-  "message": "Messsage for NAMESERVER/AXFR_FAILURE in the language:fr",
-  "level": "NOTICE",
+   "jsonrpc": "2.0",
+   "id": 147559211994909,
+   "result": {
+      "nb_finished": 5,
+      "finished_test_ids": [
+         "43b408794155324b",
+         "be9cbb44fff0b2a8",
+         "62f487731116fd87",
+         "692f8ffc32d647ca",
+         "6441a83fcee8d28d"
+      ],
+      "nb_running": 195
+   }
 }
 ```
 
-The **module** serves to group the tests by categories.
 
-The **ns** attribute serves to show the name servers for the category
-NAMESERVER.
+#### `"params"`
 
-The **message** is the message to show.
+A *batch id*.
 
-The **level** is the level of severity of the message
 
- -  NOTICE, INFO are considered OK: green
- -  WARNING as warning: orange
- -  ERROR as error: red
+#### `"result"`
 
-### *JSON-RPC Call 8*: get\_test\_history
-This API takes the usual fronted "params" structure and uses it to return a list of results for the same domain in the same frontend tab. Currently the presence of the "nameservers" parameter is used to differentiate tests run through the "simple domain test tab" from the "undelegated domain test tab".
+An object with the following properties:
 
-**Request**:
-```
+* `"nb_finished"`: an integer. The number of finished tests.
+* `"nb_running"`: an integer. The number of running tests.
+* `"finished_test_ids"`: a list of *test ids*. The set of finished *tests* in this *batch*.
+
+
+#### `"error"`
+
+>
+> TODO: List all possible error codes and describe what they mean enough for clients to know how react to them.
+>
+
+
+## API method: `validate_syntax`
+
+Checks the `"params"` structure for syntax coherence. It is very strict on what
+is allowed and what is not to avoid any SQL injection and cross site scripting
+attempts. It also checks the domain name for syntax to ensure the domain name
+seems to be a valid domain name and a test by the *Engine* can be started.
+
+Example request:
+```json
 {
-   "jsonrpc" : "2.0",
-   "method" : "get_test_history",
-   "id" : 143014516615786,
-   "params" : {
-      "offset" : 0,
-      "limit" : 200,
-      "frontend_params" : {
-         "nameservers" : [
-            {
-               "ip" : "192.134.4.1",
-               "ns" : "ns1.nic.FR."
-            },
-            {
-               "ip" : "2001:660:3003:2:0:0:4:1",
-               "ns" : "ns1.nic.FR."
-            },
-            {
-               "ns" : "ns3.nic.FR.",
-               "ip" : "192.134.0.49"
-            },
-            {
-               "ns" : "ns3.nic.FR.",
-               "ip" : "2001:660:3006:1:0:0:1:1"
-            },
+    "jsonrpc": "2.0",
+    "id": 143014426992009,
+    "method": "validate_syntax",
+    "params": {
+        "domain": "zonemaster.net",
+        "ipv6": 1,
+        "ipv4": 1,
+        "nameservers": [
             {
-               "ip" : "192.93.0.4",
-               "ns" : "ns2.nic.FR."
+                "ns": "ns1.nic.fr",
+                "ip": "1.2.3.4"
             },
             {
-               "ns" : "ns2.nic.FR.",
-               "ip" : "2001:660:3005:1:0:0:1:2"
+                "ns": "ns2.nic.fr",
+                "ip": "192.134.4.1"
             }
-         ],
-         "ipv4" : true,
-         "profile" : "default_profile",
-         "ipv6" : true,
-         "advanced" : true,
-         "domain" : "afnic.FR",
-         "ds_info" : []
-      }
-   }
+        ]
+    }
 }
-
 ```
 
- -  params: an object containing the following parameters
-    -  frontend\_params: the usual structure containing all the
-       parameters of the interface
-    -  offset: the start of pagination (not yet supported) (optional, default 0)
-    -  limit: number of items to return (not yet supported) (optional, default 200)
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -   method: the name of the called method
-
-**Response**:
-```
+Example response:
+```json
 {
-  "jsonrpc": "2.0",
-  "id": 140743003648550,
-  "result": [
-    {
-      "advanced_options": "1",
-      "id": 3,
-      "creation_time": "2014-08-05 19:41:14.522656",
-      "overall_result" : "error"
-    },
-    {
-      "advanced_options": "1",
-      "id": 1,
-      "creation_time": "2014-08-05 11:48:18.542216",
-      "overall_result" : "warning"
+    "jsonrpc": "2.0",
+    "id": 143014426992009,
+    "result": {
+        "status": "ok",
+        "message": "Syntax ok"
     }
-  ]
 }
 ```
 
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  result: an ordered (starting by the most recent test) list of tests
-    with
-    -  id: the id to use to retrieve the test result
-    -  creation\_date: the date of test
-    -  advanced\_options: if set to 1 serves to differentiate tests
-       with advanced options from those without this option.
-    - overall\_result: shows if there were any errors or warnings in the result (for color differentiation in the test results history)
 
-## Batch mode API (Experimental as of now)
+#### `"params"`
 
-### *JSON-RPC Call*: `add_api_user`
+An object with the following properties:
 
-**Request**:
-```
-{
-    "jsonrpc": "2.0",
-    "id": 4711,
-    "method": "add_api_user",
-    "params": {
-        "username": "citron",
-        "api_key": "fromage"
-    }
-}
-```
+* `"domain"`: a *domain name*.
+* `"ipv4"`: an optional `1`, `0`, `true` or `false`.
+* `"ipv6"`: an optional `1`, `0`, `true` or `false`.
+* `"ds_info"`: an optional list of *DS info* objects.
+* `"nameservers"`: an optional list of objects each of *name server* objects.
+* `"profile"`: an optional *profile name*.
+* `"advanced"`: an optional `true` or `false`.
+* `"client_id"`: ...
+* `"client_version"`: ...
+* `"user_ip"`: ...
+* `"user_location_info"`: ...
+* `"config"`: ...
 
- -  params: an object containing the following parameters
-    -  username: the name of the user to add
-    -  api_key: the API key (in effect, password) for the user to add
- -   jsonrpc: « 2.0 »
- -   id: any kind of unique id allowing to match requests and responses
- -   method: the name of the called method
+If the `"nameservers"` key is _not_ set, a recursive query made by the
+server to its locally configured resolver for NS records for the
+value of the `"domain"` key must return a reply with at least one
+resource record in the Answer Section.
 
-**Response**:
-```
-{
-  "jsonrpc": "2.0",
-  "id": 4711
-  "result": 1
-}
-```
+At least one of `"ipv4"` and `"ipv6"` must be present and either `1` or `true`.
+
+>
+> TODO: Clarify the data type of the following `"params"` properties:
+> `"client_id"`, `"client_version"`, `"user_ip"`, `"user_location_info"` and
+> `"config"`.
+>
+> TODO: Clarify the purpose of each `"params"` property.
+>
+
+
+#### `"result"`
+
+An object with the following properties:
+
+* `"status"`: either `"ok"` or `"nok"`.
+* `"message"`: a string. Human-readable details about the status.
+
+#### `"error"`
+
+>
+> TODO: List all possible error codes and describe what they mean enough for clients to know how react to them.
+>
+
+
+## API method: `get_test_params`
+
+>
+> TODO: Method description
+>
+> TODO: Example request
+>
+> TODO: Example response
+>
+
+
+#### `"params"`
+
+A *test id*.
+
+
+#### `"result"`
+
+The `"params"` object sent to `start_domain_test` when the *test* was started.
+
+>
+> TODO: What about if the *test* was created with `add_batch_job` or something else?
+>
+
+
+#### `"error"`
 
- -  jsonrpc: « 2.0 »
- -  id: any kind of unique id allowing to match requests and responses
- -  result: The number of users created.
-       
+>
+> TODO: List all possible error codes and describe what they mean enough for clients to know how react to them.
+>
diff --git a/docs/Architecture.md b/docs/Architecture.md
new file mode 100644
index 0000000..2075f34
--- /dev/null
+++ b/docs/Architecture.md
@@ -0,0 +1,86 @@
+# Architecture
+
+The Zonemaster *Backend* is a system for performing domain health checks and
+keeping records of performed domain health checks.
+
+A Zonemaster *Backend* system consists of at least three components: a
+single *Database*, a single *Worker* and one or more *Web backends*.
+
+
+## Components
+
+### Database
+
+The *Database* stores health check requests and results. The *Backend*
+architecture is oriented around a single central *Database*.
+
+
+### Worker
+
+A Zonemaster *Worker* is a daemon that picks up *test* requests from the
+*Database*, runs them using the *Engine* library, and records the results back
+to the *Database*. A single *Worker* may handle several requests concurrently.
+The *Backend* architecture supports a single *Worker* interacting with a single *Database*.
+
+>
+> TODO: List all files these processes read and write.
+>
+> TODO: List everything these processes open network connections to.
+>
+> TODO: Describe in which order *test* are processed.
+>
+> TODO: Describe how concurrency, parallelism and synchronization works within a single *Worker*.
+>
+> TODO: Describe how synchronization works among parallel *Workers*.
+>
+
+
+### Web backend
+
+A Zonemaster *Web backend* is a daemon providing a JSON-RPC interface for
+recording *test* requests in the *Database* and fetching *test* results from the
+*Database*. The *Backend* architecture supports multiple *Web backends*
+interacting with the same *Database*.
+
+>
+> TODO: List all ports these processes listen to.
+>
+> TODO: List all files these processes read and write.
+>
+> TODO: List everything these processes open network connections to.
+>
+
+
+## Glossary
+
+### Test
+
+### Batch
+
+### Test result
+
+### Test module
+
+### Message
+
+### Policy
+
+### Profile
+
+>
+> TODO: Come up with a better name to distinguish it from *config profiles*.
+>
+
+### Config profile
+
+*Config profiles* are configured under the the `ZONEMASTER` section of `zonemaster_backend.ini`.
+
+>
+> TODO: Describe this in greater detail.
+>
+
+
+### Engine
+
+The Zonemaster *Engine* is a library for performing *tests*. It's hosted in [its
+own repository](https://github.com/dotse/zonemaster-engine/).
diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md
new file mode 100644
index 0000000..c313e3e
--- /dev/null
+++ b/docs/GettingStarted.md
@@ -0,0 +1,47 @@
+# Getting started
+
+This is a guide for getting started with the Zonemaster Web Backend JSON-RPC API.
+
+>
+> Note: This guide makes a number of assumptions about you setup:
+>
+> * that it's a Unix-like environment
+> * that you have Zonemaster *backend* installed according to the [installation guide](installation.md)
+> * that you have the tools `curl` and `jq` installed
+>
+
+First, check that the *Web backend* is running and answering properly.
+
+```sh
+curl -sS -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "version_info"}' http://localhost:5000/ | jq .
+```
+
+Enqueue a *test* of the domain `zonemaster.net`.
+
+```sh
+curl -sS -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 2, "method": "start_domain_test", "params": {"domain": "zonemaster.net", "ipv4": true}}' http://localhost:5000/ | jq .
+```
+
+However, we need the *test id* of the *test* we just enqueued.
+Let's query the same method with the same params again, let `jq` filter out the *test id* for us, and then store it in an environment variable.
+
+```sh
+TESTID=`curl -sS -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 3, "method": "start_domain_test", "params": {"domain": "zonemaster.net", "ipv4": true}}' http://localhost:5000/ | jq .result`
+echo "$TESTID"
+```
+
+Watch the *test* progress (`"result"`) up to `100` (percent) by repeatedly running this command.
+
+```sh
+curl -sS -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 4, "method": "test_progress", "params": '"$TESTID"'}' http://localhost:5000/ | jq .
+```
+
+Once the progress value has reached `100`, you can query for the *test result*.
+
+```sh
+curl -sS -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 5, "method": "get_test_results", "params": {"id": '"$TESTID"'}}' http://localhost:5000/ | jq .
+```
+
+If you're moderatly quick and repeatedly re-run the last command you should be able to see the progress value increase in uneven steps from `0` to `100`.
+Never mind updating the JSON-RPC `"id"` - the server doesn't care.
+Once the progress has reached 100, lots of test results should also be showing up.
diff --git a/docs/TypographicConventions.md b/docs/TypographicConventions.md
new file mode 100644
index 0000000..132ead5
--- /dev/null
+++ b/docs/TypographicConventions.md
@@ -0,0 +1,25 @@
+# Typographic conventions
+
+The Zonemaster Backend documentation uses the following typographic conventions:
+
+*Italic* text is used for:
+
+* technical terms defined in the [Architecture](Architecture.md) document
+* data types defined in the [API](API.md) document
+
+`Monospace` text is used for:
+
+* snippets of JSON or sh
+* JSON-RPC method names
+* JSON values
+* single or strings of characters
+* internet addresses (e.g. domain names and IP addresses)
+* file names with or without paths (e.g. configuration files and command line
+  tools)
+* config section names
+
+>
+> Block quotes are used for:
+>
+> * notes and commentary
+>
diff --git a/docs/installation.md b/docs/installation.md
index df9d59a..34c5a90 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -1,318 +1,549 @@
-# Zonemaster Backend Installation instructions
+# Zonemaster Backend installation guide
 
-The documentation covers the following operating systems:
+## Overview
 
- * [1] <a href="#Debian">Ubuntu 14.04 (LTS)</a>
- * [2] <a href="#Debian">Debian Wheezy (version 7)</a>
- * [3] <a href="#FreeBSD">FreeBSD 10</a>
- * [4] <a href="#CentOS">CentOS 7.1</a>
+Zonemaster *Backend* needs to run on an operating system. One can choose any of
+the following OS to install the *Backend* after having the required
+[Prerequisites](#prerequisites).
 
-## Pre-Requisites
+* <a href="#centos">CentOS 7</a>  
+* <a href="#debian">Debian 8 (Jessie)</a>  
+* <a href="#debian">Ubuntu 16.04</a>  
+* <a href="#freebsd">FreeBSD 10.3</a>  
 
-Zonemaster-engine should be installed before. Follow the instructions
-[here](https://github.com/dotse/zonemaster-engine/blob/master/docs/installation.md)
+>
+> Note: We assume the installation instructions will work for earlier OS
+> versions too. If you have any issue in installing the Zonemaster engine with
+> earlier versions, please send a mail with details to contact at zonemaster.net 
+>
 
-## Preambule
-    
-   To install the backend following steps are needed based on your chosen distribution:
- 
-   * Install the package dependencies and CPAN dependencies if any
-   * Clone the software from git and install it
-   * Two databases are available for the backend : 1. PostgreSQL and 2.MySQL. BAsed on your chosen database, configure. 
-   * Start the backend and verify whether it has been started
-   * Start the part in the backend that can communicate with JSON::RPC
+In addition, Zonemaster *Backend* needs a database engine. The choice for the database are
+as follows :
+
+* MySQL 
+* PostgreSQL 9.3 or higher 
+* SQLite 
 
+## Prerequisites
 
-## <a name="Debian"></a> Instructions for installing in Ubuntu 14.04 and Debian wheezy (version 7)
+This guide assumes that the following softwares are already installed on the
+target system :
 
-1) Install package dependencies
+* the chosen operating system 
+* curl (only for post-installation sanity check)
+* [Zonemaster Engine](https://github.com/dotse/zonemaster-engine/blob/master/docs/installation.md) is installed 
 
-    sudo apt-get update
+## <a name="centos"></a>1. CentOS 
 
-    sudo apt-get install git libmodule-install-perl libconfig-inifiles-perl libdbd-sqlite3-perl starman libio-captureoutput-perl libproc-processtable-perl libstring-shellquote-perl librouter-simple-perl libclass-method-modifiers-perl libtext-microtemplate-perl libdaemon-control-perl 
+### 1.1 Installing dependencies 
 
-2) Install CPAN dependencies
+```sh 
+sudo yum install perl-Module-Install perl-IO-CaptureOutput perl-String-ShellQuote 
+sudo cpan -i Config::IniFiles Daemon::Control JSON::RPC::Dispatch Parallel::ForkManager Plack::Builder Plack::Middleware::Debug Router::Simple::Declare Starman 
+```
 
-    sudo cpan -i Plack::Middleware::Debug Parallel::ForkManager JSON::RPC
+### 1.2 Install the chosen database engine and related dependencies.
 
-Note: The Perl modules `Parallel::ForkManager` and `JSON::RPC` exist as Debian packages, but with versions too old to be useful for us.
+#### 1.2.1 MySQL
 
-3) Get the source code
+```sh 
+sudo yum install wget 
+wget http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm 
+sudo rpm -ivh mysql-community-release-el7-5.noarch.rpm 
+sudo yum install mysql-server perl-DBD-mysql 
+sudo systemctl start mysqld 
+```
 
-    git clone https://github.com/dotse/zonemaster-backend.git
+Verify that MySQL has started 
 
-4) Build source code
+```sh
+service mysqld status
+```
 
-    cd zonemaster-backend
-    perl Makefile.PL
-    make
-    make test
+#### 1.2.2 PostgreSQL
 
-Both these steps produce quite a bit of output. As long as it ends by
-printing `Result: PASS`, everything is OK.
+>
+> At this time there is no instruction for using PostgreSQL on CentOS.
+>
 
-5) Install 
+#### 1.2.3 SQLite
 
-    sudo make install
+>
+> At this time there is no instruction for using SQLite on CentOS.
+>
 
-This too produces some output. The `sudo` command may not be necessary,
-if you normally have write permissions to your Perl installation.
+### 1.3 Installation of the backend
 
-## Database set up
+```sh
+sudo cpan -i Zonemaster::WebBackend
+```
 
-### Using PostgreSQL as database for the backend
+### 1.4 Directory and file manipulation
 
-1) Create a directory 
+```sh
+sudo mkdir /etc/zonemaster
+mkdir "$HOME/logs"
+```
 
-    sudo mkdir /etc/zonemaster
+The Zonemaster::WebBackend module installs a number of configuration files in a
+shared data directory.  This section refers to the shared data directory as the
+current directory, so locate it and go there like this:
 
-2) Edit the file `share/backend_config.ini` in the `zonemaster-backend`
-directory
+```sh
+cd `perl -MFile::ShareDir -le 'print File::ShareDir::dist_dir("Zonemaster-WebBackend")'`
+```
 
-    engine           = PostgreSQL
-    user             = zonemaster
-    password         = zonemaster
-    database_name    = zonemaster
-    database_host    = localhost
-    polling_interval = 0.5
-    log_dir          = logs/
-    interpreter      = perl
-    max_zonemaster_execution_time   = 300
-    number_of_professes_for_frontend_testing  = 20
-    number_of_professes_for_batch_testing     = 20
+Copy the `backend_config.ini` file to `/etc/zonemaster`.
 
-3) Copy the `backend_config.ini` file to `/etc/zonemaster`
+```sh
+sudo cp ./backend_config.ini /etc/zonemaster/
+```
 
-    sudo cp share/backend_config.ini /etc/zonemaster
+### 1.5 Service script set up
+Copy the example init file to the system directory.  You may wish to edit the
+file in order to use a more suitable user and group.  As distributed, it uses
+the MySQL user and group, since we can be sure that exists and it shouldn't mess
+up anything included with the system.
 
-4) PostgreSQL Database manipulation 
+```sh
+sudo cp ./zm-centos.sh /etc/init.d/
+sudo chmod +x /etc/init.d/zm-centos.sh
+```
+### 1.6 Chosen database configuration
 
-   **Make sure that the PostgreSQL version is 9.3 or higher**
+#### 1.6.1 MySQL
 
-    psql --version
+Edit the file `/etc/zonemaster/backend_config.ini`.
 
-5) Install PostgreSQL packages
+```
+engine           = MySQL
+user             = zonemaster
+password         = zonemaster
+database_name    = zonemaster
+database_host    = localhost
+polling_interval = 0.5
+log_dir          = logs/
+interpreter      = perl
+max_zonemaster_execution_time   = 300
+number_of_processes_for_frontend_testing = 20
+number_of_processes_for_batch_testing    = 20
+```
 
-    sudo apt-get install libdbd-pg-perl postgresql
+Using a database adminstrator user (called root in the example below), run the
+setup file:
 
-6) Connect to Postgres as a user with administrative privileges and set things up:
-   
-    sudo su - postgres
-    psql -f /home/<user>/zonemaster-backend/docs/initial-postgres.sql
+```sh
+mysql --user=root --password < ./initial-mysql.sql
+```
 
-    Make sure that ** <user> ** in the above path is modified appropriately
+This creates a database called `zonemaster`, as well as a user called
+"zonemaster" with the password "zonemaster" (as stated in the config file). This
+user has just enough permissions to run the backend software.
 
-This creates a database called `zonemaster`, as well as a user called "zonemaster" with the password "zonemaster" (as stated in the config file). This user has just enough permissions to run the backend software.
+>
+> Note : Only run the above command during an initial installation of the
+> Zonemaster backend. If you do this on an existing system, you will wipe out the
+> data in your database.
+>
 
+ 
 If, at some point, you want to delete all traces of Zonemaster in the database,
-you can run the file `docs/cleanup-postgres.sql` in the `zonemaster-backend`
-directory as a database administrator. It removes the user and drops the database (obviously taking all data with it).
+you can run the file `cleanup-mysql.sql` as a database administrator. Commands
+for locating and running the file are below. It removes the user and drops the
+database (obviously taking all data with it).
+ 
+
+```sh
+cd `perl -MFile::ShareDir -le 'print File::ShareDir::dist_dir("Zonemaster-WebBackend")'`
+mysql --user=root --password < ./cleanup-mysql.sql
+```
+
+
+#### 1.6.2 PostgreSQL
+
+>
+> At this time there is no instruction for creating a database in PostgreSQL.
+>
+
+Edit the file `/etc/zonemaster/backend_config.ini`.
+
+```
+engine           = PostgreSQL
+user             = zonemaster
+password         = zonemaster
+database_name    = zonemaster
+database_host    = localhost
+polling_interval = 0.5
+log_dir          = logs/
+interpreter      = perl
+max_zonemaster_execution_time   = 300
+number_of_processes_for_frontend_testing = 20
+number_of_processes_for_batch_testing    = 20
+```
+
+
+#### 1.6.3 SQLite
+
+>
+> At this time there is no instruction for configuring/creating a database in PostgreSQL.
+>
+
+### 1.7 Service startup
+
+```sh
+sudo systemctl start zm-centos
+```
+
+### 1.8 Post-installation sanity check
+
+If you followed this instructions to the letter, you should be able to use the
+API on localhost port 5000, like this:
+
+```sh
+curl -s -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"version_info","id":"1"}' http://localhost:5000/ && echo
+```
 
-8) Exit PostgreSQL
+The command is expected to give an immediate JSON response similiar to :
 
-   exit
+```sh
+{"id":140715758026879,"jsonrpc":"2.0","result":"Zonemaster Test Engine Version: v1.0.2"}
+```
 
-### Using MySQL as database for the backend
+## <a name="debian"></a>2. Ubuntu & Debian 
 
-1) Create a directory 
+### 2.1 Installing dependencies
 
-    sudo mkdir /etc/zonemaster
+```sh
+sudo apt-get update
+sudo apt-get install git libmodule-install-perl libconfig-inifiles-perl libdbd-sqlite3-perl starman libio-captureoutput-perl libproc-processtable-perl libstring-shellquote-perl librouter-simple-perl libclass-method-modifiers-perl libtext-microtemplate-perl libdaemon-control-perl
+sudo cpan -i  Test::Requires Plack::Middleware::Debug Parallel::ForkManager JSON::RPC 
+```
+>
+> Note: The Perl modules `Parallel::ForkManager` and `JSON::RPC` exist as Debian
+> packages, but with versions too old to be useful for us.
+>
 
-2) Edit the file `share/backend_config.ini`
+### 2.2 Install the chosen database engine and related dependencies
 
-    engine           = MySQL
-    user             = zonemaster
-    password         = zonemaster
-    database_name    = zonemaster
-    database_host    = localhost
-    polling_interval = 0.5
-    log_dir          = logs/
-    interpreter      = perl
-    max_zonemaster_execution_time   = 300
-    number_of_professes_for_frontend_testing  = 20
-    number_of_professes_for_batch_testing     = 20
+#### 2.2.1 MySQL
 
-3)  Copy the `backend_config.ini` file to `/etc/zonemaster`
+```sh
+sudo apt-get install mysql-server libdbd-mysql-perl
+```
 
-    sudo cp share/backend_config.ini /etc/zonemaster
+#### 2.2.2 PostgreSQL
 
-4) Install MySQL packages.
+```sh
+sudo apt-get install libdbd-pg-perl postgresql
+```
 
-    sudo apt-get install mysql-server libdbd-mysql-perl
+#### 2.2.3 SQLite
 
-5) Using a database adminstrator user (called root in the example below), run the setup file:
-    
-    mysql --user=root --password < docs/initial-mysql.sql
-    
-This creates a database called `zonemaster`, as well as a user called "zonemaster" with the password "zonemaster" (as stated in the config file). This user has just enough permissions to run the backend software.
+>
+> At this time there is no instruction for using SQLite on Debian and Ubuntu.
+>
 
-If, at some point, you want to delete all traces of Zonemaster in the database, you can run the file `docs/cleanup-mysql.sql` as a database administrator. It removes the user and drops the database (obviously taking all data with it).
+### 2.3 Installation of the backend
 
-### Starting the backend
+```sh
+sudo cpan -i Zonemaster::WebBackend
+```
+### 2.4 Directory and file manipulation
 
-1) Create a log directory. 
+```sh
+sudo mkdir /etc/zonemaster
+mkdir "$HOME/logs"
+```
 
-    cd ~/
-    mkdir logs
+The Zonemaster::WebBackend module installs a number of configuration files in a
+shared data directory.  This section refers to the shared data directory as the
+current directory, so locate it and go there like this:
 
-2) In all the examples below, replace **`/home/user`** with the path to your own homedirectory (or, of course, wherever you want).
+```sh
+cd `perl -MFile::ShareDir -le 'print File::ShareDir::dist_dir("Zonemaster-WebBackend")'`
+```
 
-    starman --error-log=/home/user/logs/backend_starman.log --listen=127.0.0.1:5000 --pid=/home/user/logs/starman.pid --daemonize /usr/local/bin/zonemaster_webbackend.psgi
+Copy the `backend_config.ini` file to `/etc/zonemaster`.
 
-3) To verify starman has started:
+```sh
+sudo cp ./backend_config.ini /etc/zonemaster/
+```
+### 2.5 Service script set up
 
-    cat ~/logs/backend_starman.log
+Copy the file `./zm-backend.sh` to the directory `/etc/init`, make it an
+executable file, and add the file to start up script.
 
-4) If you would like to kill the starman process, you can issue this command:
+```sh
+sudo cp ./zm-backend.sh /etc/init.d/
+sudo chmod +x /etc/init.d/zm-backend.sh
+sudo update-rc.d zm-backend.sh defaults
+```
 
-    kill `cat /home/user/logs/starman.pid`
+### 2.6 Chosen database configuration
 
-### Starting the starman part that listens for and answers the JSON::RPC requests 
+#### 2.6.1 MySQL
 
-1)  Copy the file `share/zm-backend.sh` to the directory `/etc/init`.
+Edit the file `/etc/zonemaster/backend_config.ini`.
 
-    sudo cp share/zm-backend.sh /etc/init.d/
+```
+engine           = MySQL
+user             = zonemaster
+password         = zonemaster
+database_name    = zonemaster
+database_host    = localhost
+polling_interval = 0.5
+log_dir          = logs/
+interpreter      = perl
+max_zonemaster_execution_time   = 300
+number_of_processes_for_frontend_testing = 20
+number_of_processes_for_batch_testing    = 20
+```
 
-2)  Make it an executable file
+Using a database adminstrator user (called root in the example below), run the
+setup file:
 
-    sudo chmod +x /etc/init.d/zm-backend.sh
+```sh
+mysql --user=root --password < ./initial-mysql.sql
+```
 
-3)  Add the file to start up script
+This creates a database called `zonemaster`, as well as a user called
+"zonemaster" with the password "zonemaster" (as stated in the config file). This
+user has just enough permissions to run the backend software.
 
-    sudo update-rc.d zm-backend.sh defaults
+>
+> Note : Only run the above command during an initial installation of the
+> Zonemaster backend. If you do this on an existing system, you will wipe out
+> the
+> data in your database.
+>
 
-4)  Start the process
+If, at some point, you want to delete all traces of Zonemaster in the database,
+you can run the file `cleanup-mysql.sql` as a database administrator. Commands
+for locating and running the file are below. It removes the user and drops the
+database (obviously taking all data with it).
+
+
+```sh
+cd `perl -MFile::ShareDir -le 'print File::ShareDir::dist_dir("Zonemaster-WebBackend")'`
+mysql --user=root --password < ./cleanup-mysql.sql
+```
+
+#### 2.6.2 PostgreSQL
+
+Edit the file `/etc/zonemaster/backend_config.ini`.
+
+```sh
+engine           = PostgreSQL
+user             = zonemaster
+password         = zonemaster
+database_name    = zonemaster
+database_host    = localhost
+polling_interval = 0.5
+log_dir          = logs/
+interpreter      = perl
+max_zonemaster_execution_time   = 300
+number_of_processes_for_frontend_testing  = 20
+number_of_processes_for_batch_testing     = 20
+```
+
+Connect to Postgres as a user with administrative privileges and set things up:
+
+```sh
+sudo -u postgres psql -f ./initial-postgres.sql
+```
+
+This creates a database called `zonemaster`, as well as a user called
+"zonemaster" with the password "zonemaster" (as stated in the config file). This
+user has just enough permissions to run the backend software.
 
-    sudo service zm-backend.sh start
+#### 2.6.3 SQLite
+
+>
+> At this time there is no instruction for configuring and creating a database
+> in SQLite.
+>
+
+### 2.7 Service startup
+
+Starting the starman part that listens for and answers the JSON::RPC requests
+
+```sh
+sudo service zm-backend.sh start
+```
 
 This only needs to be run as root in order to make sure the log file can be
 opened. The `starman` process will change to the `www-data` user as soon as it
 can, and all of the real work will be done as that user.
 
-## Testing the setup
+Check that the service has started 
+
+```sh
+sudo service zm-backend.sh status
+```
+
+### 2.8 Post-installation sanity check
+
+If you followed this instructions to the letter, you should be able to use the
+API on localhost port 5000, like this:
+
+```sh
+curl -s -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"version_info","id":"1"}' http://localhost:5000/ && echo
+```
+
+The command is expected to give an immediate JSON response similiar to :
 
-You can look into the [API documentation](API.md) to see how you can use the
-API for your use. If you followed the instructions to the minute, you should
-be able to use the API och localhost port 5000, like this:
+```sh
+{"id":140715758026879,"jsonrpc":"2.0","result":"Zonemaster Test Engine Version: v1.0.2"}
+```
 
-    curl -H "Content-Type: application/json" -d '{"params":"","jsonrpc":"2.0","id":140715758026879,"method":"version_info"}' http://localhost:5000/
+## <a name="freebsd"></a>3. FreeBSD
 
-The response should be something like this:
+### 3.1 Become superuser
+To do most of the following steps you have to be superuser (root). Change to
+root and then execute the steps for FreeBSD.
 
-    {"id":140715758026879,"jsonrpc":"2.0","result":"Zonemaster Test Engine Version: v1.0.2"}
+su
 
-### All done
+### 3.2 Installing dependencies
 
-Next step is to install the [Web UI](https://github.com/dotse/zonemaster-gui/blob/master/docs/installation.md) if you wish so.
+```sh
+pkg install p5-Config-IniFiles p5-DBI p5-File-Slurp p5-HTML-Parser p5-IO-CaptureOutput p5-JSON p5-JSON-RPC p5-Locale-libintl p5-libwww p5-Moose p5-Plack p5-Router-Simple p5-String-ShellQuote p5-Starman p5-File-ShareDir p5-Parallel-ForkManager p5-Daemon-Control p5-Module-Install p5-DBD-SQLite p5-Plack-Middleware-Debug
+``` 
 
-## <a name="FreeBSD"></a> FreeBSD 10.0 & 10.1 Instructions
+### 3.3 Install the chosen database engine and related dependencies
 
-First, make sure your operating system and package database is up to date.
+#### 3.3.1 MySQL
 
-1) Become root
+```sh
+pkg install mysql56-server p5-DBD-mysql
+```
+>
+> At this time there is no instruction for configuring/starting MySQL on FreeBSD.
+>
 
-    su -
+#### 3.3.2 PostgreSQL
 
-2) Install packages
+```sh
+pkg install postgresql93-server p5-DBD-Pg
+echo 'postgresql_enable="YES"' | sudo tee -a /etc/rc.conf
+service postgresql initdb
+service postgresql start
+```
 
-    pkg install p5-Config-IniFiles p5-DBI p5-File-Slurp p5-HTML-Parser p5-IO-CaptureOutput p5-JSON p5-JSON-RPC p5-Locale-libintl p5-libwww p5-Moose p5-Plack p5-Router-Simple p5-String-ShellQuote p5-Starman p5-File-ShareDir p5-Parallel-ForkManager p5-Daemon-Control p5-Module-Install p5-DBD-SQLite p5-Plack-Middleware-Debug
+#### 3.3.3 SQLite
 
-3) Get and build the source code
+>
+> At this time there is no instruction for using SQLite on FreeBSD.
+>
 
-    git clone https://github.com/dotse/zonemaster-backend.git
-    cd zonemaster-backend
-    perl Makefile.PL
-    make
-    make test
-    make install
+### 3.4 Installation of the backend
 
-### Database installation and setup (currently PostgreSQL and MySQL supported)
+```sh
+cpan -i Zonemaster::WebBackend
+```
 
-4.1) PostgreSQL
+### 3.5 Directory and file manipulation
 
-    sudo pkg install postgresql93-server p5-DBD-Pg
+```sh
+mkdir /etc/zonemaster
+mkdir "$HOME/logs"
+```
 
-4.1) Start the PostgreSQL server according to its instructions then initiate the database using the following script.
+The Zonemaster::WebBackend module installs a number of configuration files in a
+shared data directory.  This section refers to the shared data directory as the
+current directory, so locate it and go there like this:
 
-    psql -U pgsql template1 -f docs/initial-postgres.sql
+```sh
+cd `perl -MFile::ShareDir -le 'print File::ShareDir::dist_dir("Zonemaster-WebBackend")'`
+```
 
-4.2) MySQL
+Copy the `backend_config.ini` file to `/etc/zonemaster`.
 
-    pkg install mysql56-server p5-DBD-mysql
+```sh
+cp ./backend_config.ini /etc/zonemaster/
+```
 
-4.2) Start the MySQL server according to its instructions then initiate the database using the following script.
+### 3.6 Service script set up
 
-    mysql -uroot < docs/initial-mysql.sql
+>
+> At this time there is no instruction for running Zonemaster Web backends
+> nor Workers as services on FreeBSD.
+>
 
-5) Configure Zonemaster-Backend to use the chosen database
+### 3.7 Chosen database configuration
 
-    mkdir -p /etc/zonemaster
-    cp share/backend_config.ini /etc/zonemaster/
+#### 3.7.1 MySQL
 
-6) Edit the "engine" line to match the chosen database, MySQL and PostgreSQL supported.
+>
+> At this time there is no instruction for configuring and creating a database
+> in SQLite.
+>
 
-   vi /etc/zonemaster/backend_config.ini
+#### 3.7.2 PostgreSQL
 
-7) Start the processes, point pid and log to a appropriate-for-your-OS location
-   (first line is the API, second is the test runner itself)
+Edit the file `/etc/zonemaster/backend_config.ini`.
 
-    starman --error-log=/home/user/logs/error.log --pid-file=/home/user/logs/starman.pid --listen=127.0.0.1:5000 --daemonize /usr/local/bin/zonemaster_webbackend.psgi
-    zm_wb_daemon start
+```
+engine           = PostgreSQL
+user             = zonemaster
+password         = zonemaster
+database_name    = zonemaster
+database_host    = localhost
+polling_interval = 0.5
+log_dir          = logs/
+interpreter      = perl
+max_zonemaster_execution_time   = 300
+number_of_processes_for_frontend_testing = 20
+number_of_processes_for_batch_testing    = 20
+```
 
-### All done
+Start the PostgreSQL server according to its instructions then initiate the
+database using the following script.
 
-Next step is to install the [Web
-UI](https://github.com/dotse/zonemaster-gui/blob/master/docs/installation.md) if
-you wish so.
+```sh
+psql -U pgsql -f ./initial-postgres.sql template1
+```
 
-## <a name="CentOS"></a> CentOS instructions (Version 7.1)
+#### 3.7.3 SQLite
 
-1) Install the Zonemaster test engine according to its instructions.
+>
+> At this time there is no instruction for configuring and creating a database
+> in SQLite.
+>
 
-2) Add packages.
-    
-    sudo yum install perl-Module-Install perl-IO-CaptureOutput perl-String-ShellQuote
+### 3.8 Service startup
 
-3) Install modules from CPAN.
-    
-    sudo cpan -i Config::IniFiles Daemon::Control JSON::RPC::Dispatch Parallel::ForkManager Plack::Builder Plack::Middleware::Debug Router::Simple::Declare Starman
+```sh
+starman --error-log="$HOME/logs/error.log" --pid-file="$HOME/logs/starman.pid" --listen=127.0.0.1:5000 --daemonize /usr/local/bin/zonemaster_webbackend.psgi 
+zm_wb_daemon start
+```
 
-4) Fetch the source code.
-    
-    git clone https://github.com/dotse/zonemaster-backend.git
-    cd zonemaster-backend
+### 3.9 Post-installation sanity check
 
-5) Build and install the backend modules.
-    
-    perl Makefile.PL && make test && sudo make install
+If you followed this instructions to the letter, you should be able to use the
+API on localhost port 5000, like this:
 
-6) Install a database server. MySQL, in this example.
-    
-    sudo yum install wget
-    wget http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm
-    sudo rpm -ivh mysql-community-release-el7-5.noarch.rpm
-    sudo yum install mysql-server perl-DBD-mysql
-    sudo systemctl start mysqld
+```sh
+curl -s -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"version_info","id":"1"}' http://localhost:5000/ && echo
+```
 
-7) Set up the database.
-    
-    mysql -uroot < docs/initial-mysql.sql
+The command is expected to give an immediate JSON response similiar to :
 
-8) Copy the example init file to the system directory. You may wish to edit the file in order to use a more suitable user and group. As distributed, it uses the MySQL user and group, since we can be sure that exists and it shouldn't mess up anything included with the system.
-    
-    sudo cp share/zm-centos.sh /etc/init.d/
-    sudo chmod +x /etc/init.d/zm-centos.sh
+```sh
+{"id":140715758026879,"jsonrpc":"2.0","result":"Zonemaster Test Engine Version: v1.0.2"}
+```
 
-9) Start the services.
-    
-    sudo systemctl start zm-centos
+## What to do next?
+>
+> You will have to install the GUI or look at the API documentation. We will be
+> updating this document with links on how to do that. 
+>
 
-10) Test that it started OK. The command below should print a JSON string including some information on the Zonemaster engine version.
-    
-    curl -X POST http://127.0.0.1:5000/ -d '{"method":"version_info"}'
+-------
 
-### All done
+Copyright (c) 2013 - 2016, IIS (The Internet Foundation in Sweden)  
+Copyright (c) 2013 - 2016, AFNIC  
+Creative Commons Attribution 4.0 International License
 
-Next step is to install the [Web
-UI](https://github.com/dotse/zonemaster-gui/blob/master/docs/installation.md) if
-you wish so.
+You should have received a copy of the license along with this
+work.  If not, see <http://creativecommons.org/licenses/by/4.0/>.
diff --git a/docs/notes_api.md b/docs/notes_api.md
deleted file mode 100644
index 1eba4c0..0000000
--- a/docs/notes_api.md
+++ /dev/null
@@ -1,351 +0,0 @@
-# Common
-
-All requests consist of JSON objects with four keys.
-
-1) "jsonrpc"
-
-    The value of this key is the fixed string `"2.0"`.
-
-2) "method"
-
-    The name of the method to be called.
-
-3) "id"
-
-    An id-value meant to connect a request with a response. The value
-    has no meaning, and will simply be copied to the response.
-
-4) "params"
-
-    This key holds the parameters to the method being called.
-
-All responses consist of JSON objects with three keys.
-
-1) "jsonrpc"
-
-    The value of this key is the fixed string `"2.0"`.
-
-2) "id"
-
-    An id-value meant to connect a request with a response. The value
-    has no meaning, and is simply copied from the request.
-
-3) "result"
-
-    This key holds the results returned from the method that was called.
-
-In the descriptions below, only the contents of the "params" (input)
-and "result" (output) keys are discussed, since they are the only ones
-where the content differs between different methods.
-
-# API Methods
-
-## version_info
-
-### Input
-
-None.
-
-### Output
-
-Version information string.
-
-## get_ns_ips
-
-### Input
-
-A domain name.
-
-### Output
-
-A list of objects. The objects each have a single key and value. The
-key is the domain name given as input. The value is an IP address for
-the name, or the value `0.0.0.0` if the lookup returned no A or AAAA
-records.
-
-## get_data_from_parent_zone
-
-### Input
-
-A domain name.
-
-### Output
-
-An object with two keys, `ns_list` and `ds_list`.
-
-The value of the `ns_list` key is a list of objects. Each object in
-the list has two keys, `ns` and `ip`. The values of the `ns` key is
-the domain name of a nameserver for the input name, and the value of
-the `ip` is an IP address for that nameserver name or the value
-`0.0.0.0`.
-
-As of this writing, the value of the `ds_list` key is always an empty
-list.
-
-## validate_syntax
-
-### Input
-
-The input for this method is a JSON object. The object may have the
-keys `domain`, `ipv4`, `ipv6`, `ds_info`, `nameservers`,
-`profile`, `advanced`, `client_id` and `client_version`. If any other
-key is present, an error will be returned.
-
-If the key `nameservers` exists, its value must be a list of objects
-each of which has exactly the two keys `ip` and `ns`. The value of the
-`ns` key must meet the criteria described below for the `domain`
-value, and the value of the `ip` key must be a syntactically valid
-IPv4 or IPv6 address.
-
-If the key `ds_info` exists, its value must be a list of
-objects each of which has exactly the two keys `algorithm` and
-`digest`. The value of the `algorithm` key must be either the string
-`"sha1"`, in which case the value of the `digest` key must be 40
-hexadecimal characters, or the value `"sha256"`, in which case the
-value of the `digest` key must be 64 hexadecimal characters.
-
-At least one of the keys `ipv4` and `ipv6` must exist and have one of
-the values `1`, `0`, `true` or `false`.
-
-If the key `advanced` exists, it must have one of the values `true`
-and `false`.
-
-If the key `profile` exists, it must have a value that is one of the
-three strings `"default_profile"`, `"test_profile_1"` and
-`"test_profile_2"`.
-
-The key `domain` must exist and have a value that meets the following
-criteria.
-
-1) If the value contains characters outside the ASCII character set,
-   the entire value must be possible to convert to IDNA format.
-
-2) If the value is a single character, that character must be `.`.
-
-3) The length of the value must not be greater than 254 characters.
-
-4) When the value is split at `.` characters (after IDNA conversion,
-   if needed), each component part must be at most 63 characters long.
-
-5) Each such component part must also consist only of the characters
-   `0` to `9`, `a` to `z`, `A` to `Z` and `-`.
-
-If the `nameservers` key is _not_ set, a recursive query made by the
-server to its locally configured resolver for `NS` records for the
-value of the `domain` key must return a reply with at least one
-resource record in the `answer` section.
-
-### Output
-
-An object with the two keys `message` and `status`. If all criteria
-above were met, the `status` key will have as its value the string
-`"ok"`. If not, it will have the value `"nok"`. The value of the
-`message` key is a human-readable string with more details in the
-status.
-
-## start_domain_test
-
-### Input
-
-The same as for `validate_syntax`.
-
-### Output
-
-The numeric ID of a newly started test, or a test with the same
-parameters started within the recent configurable short time.
-
-## test_progress
-
-### Input
-
-A numeric test ID.
-
-### Output
-
-An integer value (possible encoded in a string) between 0 and 100
-inclusive, describing the progress of the test in question as a
-percentage.
-
-## get_test_params
-
-### Input
-
-A numeric test ID.
-
-### Output
-
-A JSON object with the parameters used to start the test (that is, the
-input parameters to `start_domain_test`).
-
-## get_test_results
-
-### Input
-
-A JSON object with the two keys `id` and `language`. `id` is a numeric
-test ID. `language` is a string where the two first characters are a
-language code to be used by the translator. As of this writing, the
-language codes that are expected to work are `"en"`, `"sv"` and
-`"fr"`. If the code given does not work, the translator will use
-English.
-
-### Output
-
-A JSON object with a the following keys and values:
-
-* `batch_id`
-
-    The ID number of the batch of tests this one belongs to. `null` if
-    it does not belong to a batch.
-
-* `creation_time`
-
-    The time at which the test request was created.
-
-* `domain`
-
-    The name of the tested domain.
-
-* `id`
-
-    The numeric ID of the test.
-
-* `params_deterministic_hash`
-
-    An MD5 hash of the canonical JSON representation of the test
-    parameters, used internally to identify repeated test request.
-
-* `params`
-
-    The parameters used to start the test (that is, the values used as
-    input to `start_domain_test`).
-
-* `priority`
-
-    The priority of the test. Used by the backend execution daemon to
-    determine the order in which tests are run, if there are more
-    requests than available test slots.
-
-* `progress`
-
-    An integer in the interval 0 to 100 inclusive, showing the
-    percentage of the test process that has been completed.
-
-* `results`
-
-    A list of objects representing the results of the test. Each
-    object has three keys, `module`, `message` and `level`. The values
-    of them are strings. The `module` is the test module that produced
-    the result, the `level` is the severity of the message as set in
-    the policy used (that is, one of the strings `DEBUG`, `INFO`,
-    `NOTICE`, `WARNING`, `ERROR` and `CRITICAL`) and `message` is a
-    human-readable text describing that particular result.
-
-* `test_end_time`
-
-    The time at which the test was completed.
-
-* `test_start_time`
-
-    The time at which the test was started.
-
-## get_test_history
-
-### Input
-
-A JSON object with three keys, `frontend_params`, `offset` and
-`limit`. The value of `frontend_params` is an object in turn, with the
-keys `domain` and `nameservers`.
-
-The values of `limit` and `offset` will be used as-is as the
-corresponding values in SQL expressions. `domain` and `nameservers`
-will be used to look up all tests for the given domain, separated
-according to if they were started with a `nameservers` parameter or
-not.
-
-### Output
-
-A JSON object with four keys. `id` is the numeric ID of the test.
-`creation_time` is the time when the test request was created.
-`advanced_options` is true if the corresponding flag was set in the
-request. `overall_results` is the most severe problem level logged in
-the test results.
-
-## add_api_user
-
-### Input
-
-A JSON object with two keys, `username` and `api_key`. Both should be
-strings, both are simply inserted into the users table in the
-database.
-
-### Output
-
-The numeric ID of the just created user.
-
-## add_batch_job
-
-### Input
-
-A JSON object with three keys, `username`, `api_key` and
-`batch_params`. The first two are strings, and must match a pair
-previously created with `add_api_user`.
-
-`batch_params` is a JSON object. It should be exactly the same as the
-input object described for `validate_syntax`, except that instead of
-the `domain` key there should be a key `domains`. The value of this
-key should be a list of strings with the names of domains to be
-tested. All the domains will be tested using identical parameters. The
-domain names should probably obey the rules for domain names, but in
-this case no attempt is made to enforce those prior to starting the
-tests.
-
-### Output
-
-## api1
-
-### Input
-
-None.
-
-### Output
-
-A string with the version of the Perl interpreter running the backend
-(more specifically, the value of the `$]` variable in the Perl
-interpreter).
-
-# Examples
-
-## Minimal request to start a test
-
-```
-{
-    "domain": "example.org",
-    "ipv4": true
-}
-```
-
-## Non-minimal request to start a test
-
-```
-
-{
-    "domain": "example.org",
-    "ipv4": 1,
-    "ipv6": 1,
-    "ds_info": [],
-    "profile": "default_profile",
-    "advanced": false,
-    "client_id": "Documentation Example",
-    "client_version": "1.0",
-    "nameservers": [
-        {
-            "ns": "ns1.example.org",
-            "ip": "192.168.0.1"
-        },
-        {
-            "ns": "ns2.example.org",
-            "ip": "2607:f0d0:1002:51::4"
-        }
-    ]
-}
-```
diff --git a/docs/upgrade-from-1.0.x-to-1.1.x.md b/docs/upgrade-from-1.0.x-to-1.1.x.md
new file mode 100644
index 0000000..e0f4546
--- /dev/null
+++ b/docs/upgrade-from-1.0.x-to-1.1.x.md
@@ -0,0 +1,13 @@
+To upgrade from versions 1.0.x to versions 1.1.x the column 'queue' needs to be added to the database:
+
+MySQL
+
+```
+  ALTER TABLE test_results ADD queue INTEGER DEFAULT 0;
+```
+
+PostgreSQL
+
+```
+  ALTER TABLE test_results ADD queue INTEGER DEFAULT 0;
+```
diff --git a/inc/Module/Install.pm b/inc/Module/Install.pm
new file mode 100644
index 0000000..ff767fa
--- /dev/null
+++ b/inc/Module/Install.pm
@@ -0,0 +1,474 @@
+#line 1
+package Module::Install;
+
+# For any maintainers:
+# The load order for Module::Install is a bit magic.
+# It goes something like this...
+#
+# IF ( host has Module::Install installed, creating author mode ) {
+#     1. Makefile.PL calls "use inc::Module::Install"
+#     2. $INC{inc/Module/Install.pm} set to installed version of inc::Module::Install
+#     3. The installed version of inc::Module::Install loads
+#     4. inc::Module::Install calls "require Module::Install"
+#     5. The ./inc/ version of Module::Install loads
+# } ELSE {
+#     1. Makefile.PL calls "use inc::Module::Install"
+#     2. $INC{inc/Module/Install.pm} set to ./inc/ version of Module::Install
+#     3. The ./inc/ version of Module::Install loads
+# }
+
+use 5.006;
+use strict 'vars';
+use Cwd        ();
+use File::Find ();
+use File::Path ();
+
+use vars qw{$VERSION $MAIN};
+BEGIN {
+	# All Module::Install core packages now require synchronised versions.
+	# This will be used to ensure we don't accidentally load old or
+	# different versions of modules.
+	# This is not enforced yet, but will be some time in the next few
+	# releases once we can make sure it won't clash with custom
+	# Module::Install extensions.
+	$VERSION = '1.14';
+
+	# Storage for the pseudo-singleton
+	$MAIN    = undef;
+
+	*inc::Module::Install::VERSION = *VERSION;
+	@inc::Module::Install::ISA     = __PACKAGE__;
+
+}
+
+sub import {
+	my $class = shift;
+	my $self  = $class->new(@_);
+	my $who   = $self->_caller;
+
+	#-------------------------------------------------------------
+	# all of the following checks should be included in import(),
+	# to allow "eval 'require Module::Install; 1' to test
+	# installation of Module::Install. (RT #51267)
+	#-------------------------------------------------------------
+
+	# Whether or not inc::Module::Install is actually loaded, the
+	# $INC{inc/Module/Install.pm} is what will still get set as long as
+	# the caller loaded module this in the documented manner.
+	# If not set, the caller may NOT have loaded the bundled version, and thus
+	# they may not have a MI version that works with the Makefile.PL. This would
+	# result in false errors or unexpected behaviour. And we don't want that.
+	my $file = join( '/', 'inc', split /::/, __PACKAGE__ ) . '.pm';
+	unless ( $INC{$file} ) { die <<"END_DIE" }
+
+Please invoke ${\__PACKAGE__} with:
+
+	use inc::${\__PACKAGE__};
+
+not:
+
+	use ${\__PACKAGE__};
+
+END_DIE
+
+	# This reportedly fixes a rare Win32 UTC file time issue, but
+	# as this is a non-cross-platform XS module not in the core,
+	# we shouldn't really depend on it. See RT #24194 for detail.
+	# (Also, this module only supports Perl 5.6 and above).
+	eval "use Win32::UTCFileTime" if $^O eq 'MSWin32' && $] >= 5.006;
+
+	# If the script that is loading Module::Install is from the future,
+	# then make will detect this and cause it to re-run over and over
+	# again. This is bad. Rather than taking action to touch it (which
+	# is unreliable on some platforms and requires write permissions)
+	# for now we should catch this and refuse to run.
+	if ( -f $0 ) {
+		my $s = (stat($0))[9];
+
+		# If the modification time is only slightly in the future,
+		# sleep briefly to remove the problem.
+		my $a = $s - time;
+		if ( $a > 0 and $a < 5 ) { sleep 5 }
+
+		# Too far in the future, throw an error.
+		my $t = time;
+		if ( $s > $t ) { die <<"END_DIE" }
+
+Your installer $0 has a modification time in the future ($s > $t).
+
+This is known to create infinite loops in make.
+
+Please correct this, then run $0 again.
+
+END_DIE
+	}
+
+
+	# Build.PL was formerly supported, but no longer is due to excessive
+	# difficulty in implementing every single feature twice.
+	if ( $0 =~ /Build.PL$/i ) { die <<"END_DIE" }
+
+Module::Install no longer supports Build.PL.
+
+It was impossible to maintain duel backends, and has been deprecated.
+
+Please remove all Build.PL files and only use the Makefile.PL installer.
+
+END_DIE
+
+	#-------------------------------------------------------------
+
+	# To save some more typing in Module::Install installers, every...
+	# use inc::Module::Install
+	# ...also acts as an implicit use strict.
+	$^H |= strict::bits(qw(refs subs vars));
+
+	#-------------------------------------------------------------
+
+	unless ( -f $self->{file} ) {
+		foreach my $key (keys %INC) {
+			delete $INC{$key} if $key =~ /Module\/Install/;
+		}
+
+		local $^W;
+		require "$self->{path}/$self->{dispatch}.pm";
+		File::Path::mkpath("$self->{prefix}/$self->{author}");
+		$self->{admin} = "$self->{name}::$self->{dispatch}"->new( _top => $self );
+		$self->{admin}->init;
+		@_ = ($class, _self => $self);
+		goto &{"$self->{name}::import"};
+	}
+
+	local $^W;
+	*{"${who}::AUTOLOAD"} = $self->autoload;
+	$self->preload;
+
+	# Unregister loader and worker packages so subdirs can use them again
+	delete $INC{'inc/Module/Install.pm'};
+	delete $INC{'Module/Install.pm'};
+
+	# Save to the singleton
+	$MAIN = $self;
+
+	return 1;
+}
+
+sub autoload {
+	my $self = shift;
+	my $who  = $self->_caller;
+	my $cwd  = Cwd::getcwd();
+	my $sym  = "${who}::AUTOLOAD";
+	$sym->{$cwd} = sub {
+		my $pwd = Cwd::getcwd();
+		if ( my $code = $sym->{$pwd} ) {
+			# Delegate back to parent dirs
+			goto &$code unless $cwd eq $pwd;
+		}
+		unless ($$sym =~ s/([^:]+)$//) {
+			# XXX: it looks like we can't retrieve the missing function
+			# via $$sym (usually $main::AUTOLOAD) in this case.
+			# I'm still wondering if we should slurp Makefile.PL to
+			# get some context or not ...
+			my ($package, $file, $line) = caller;
+			die <<"EOT";
+Unknown function is found at $file line $line.
+Execution of $file aborted due to runtime errors.
+
+If you're a contributor to a project, you may need to install
+some Module::Install extensions from CPAN (or other repository).
+If you're a user of a module, please contact the author.
+EOT
+		}
+		my $method = $1;
+		if ( uc($method) eq $method ) {
+			# Do nothing
+			return;
+		} elsif ( $method =~ /^_/ and $self->can($method) ) {
+			# Dispatch to the root M:I class
+			return $self->$method(@_);
+		}
+
+		# Dispatch to the appropriate plugin
+		unshift @_, ( $self, $1 );
+		goto &{$self->can('call')};
+	};
+}
+
+sub preload {
+	my $self = shift;
+	unless ( $self->{extensions} ) {
+		$self->load_extensions(
+			"$self->{prefix}/$self->{path}", $self
+		);
+	}
+
+	my @exts = @{$self->{extensions}};
+	unless ( @exts ) {
+		@exts = $self->{admin}->load_all_extensions;
+	}
+
+	my %seen;
+	foreach my $obj ( @exts ) {
+		while (my ($method, $glob) = each %{ref($obj) . '::'}) {
+			next unless $obj->can($method);
+			next if $method =~ /^_/;
+			next if $method eq uc($method);
+			$seen{$method}++;
+		}
+	}
+
+	my $who = $self->_caller;
+	foreach my $name ( sort keys %seen ) {
+		local $^W;
+		*{"${who}::$name"} = sub {
+			${"${who}::AUTOLOAD"} = "${who}::$name";
+			goto &{"${who}::AUTOLOAD"};
+		};
+	}
+}
+
+sub new {
+	my ($class, %args) = @_;
+
+	delete $INC{'FindBin.pm'};
+	{
+		# to suppress the redefine warning
+		local $SIG{__WARN__} = sub {};
+		require FindBin;
+	}
+
+	# ignore the prefix on extension modules built from top level.
+	my $base_path = Cwd::abs_path($FindBin::Bin);
+	unless ( Cwd::abs_path(Cwd::getcwd()) eq $base_path ) {
+		delete $args{prefix};
+	}
+	return $args{_self} if $args{_self};
+
+	$args{dispatch} ||= 'Admin';
+	$args{prefix}   ||= 'inc';
+	$args{author}   ||= ($^O eq 'VMS' ? '_author' : '.author');
+	$args{bundle}   ||= 'inc/BUNDLES';
+	$args{base}     ||= $base_path;
+	$class =~ s/^\Q$args{prefix}\E:://;
+	$args{name}     ||= $class;
+	$args{version}  ||= $class->VERSION;
+	unless ( $args{path} ) {
+		$args{path}  = $args{name};
+		$args{path}  =~ s!::!/!g;
+	}
+	$args{file}     ||= "$args{base}/$args{prefix}/$args{path}.pm";
+	$args{wrote}      = 0;
+
+	bless( \%args, $class );
+}
+
+sub call {
+	my ($self, $method) = @_;
+	my $obj = $self->load($method) or return;
+        splice(@_, 0, 2, $obj);
+	goto &{$obj->can($method)};
+}
+
+sub load {
+	my ($self, $method) = @_;
+
+	$self->load_extensions(
+		"$self->{prefix}/$self->{path}", $self
+	) unless $self->{extensions};
+
+	foreach my $obj (@{$self->{extensions}}) {
+		return $obj if $obj->can($method);
+	}
+
+	my $admin = $self->{admin} or die <<"END_DIE";
+The '$method' method does not exist in the '$self->{prefix}' path!
+Please remove the '$self->{prefix}' directory and run $0 again to load it.
+END_DIE
+
+	my $obj = $admin->load($method, 1);
+	push @{$self->{extensions}}, $obj;
+
+	$obj;
+}
+
+sub load_extensions {
+	my ($self, $path, $top) = @_;
+
+	my $should_reload = 0;
+	unless ( grep { ! ref $_ and lc $_ eq lc $self->{prefix} } @INC ) {
+		unshift @INC, $self->{prefix};
+		$should_reload = 1;
+	}
+
+	foreach my $rv ( $self->find_extensions($path) ) {
+		my ($file, $pkg) = @{$rv};
+		next if $self->{pathnames}{$pkg};
+
+		local $@;
+		my $new = eval { local $^W; require $file; $pkg->can('new') };
+		unless ( $new ) {
+			warn $@ if $@;
+			next;
+		}
+		$self->{pathnames}{$pkg} =
+			$should_reload ? delete $INC{$file} : $INC{$file};
+		push @{$self->{extensions}}, &{$new}($pkg, _top => $top );
+	}
+
+	$self->{extensions} ||= [];
+}
+
+sub find_extensions {
+	my ($self, $path) = @_;
+
+	my @found;
+	File::Find::find( sub {
+		my $file = $File::Find::name;
+		return unless $file =~ m!^\Q$path\E/(.+)\.pm\Z!is;
+		my $subpath = $1;
+		return if lc($subpath) eq lc($self->{dispatch});
+
+		$file = "$self->{path}/$subpath.pm";
+		my $pkg = "$self->{name}::$subpath";
+		$pkg =~ s!/!::!g;
+
+		# If we have a mixed-case package name, assume case has been preserved
+		# correctly.  Otherwise, root through the file to locate the case-preserved
+		# version of the package name.
+		if ( $subpath eq lc($subpath) || $subpath eq uc($subpath) ) {
+			my $content = Module::Install::_read($subpath . '.pm');
+			my $in_pod  = 0;
+			foreach ( split /\n/, $content ) {
+				$in_pod = 1 if /^=\w/;
+				$in_pod = 0 if /^=cut/;
+				next if ($in_pod || /^=cut/);  # skip pod text
+				next if /^\s*#/;               # and comments
+				if ( m/^\s*package\s+($pkg)\s*;/i ) {
+					$pkg = $1;
+					last;
+				}
+			}
+		}
+
+		push @found, [ $file, $pkg ];
+	}, $path ) if -d $path;
+
+	@found;
+}
+
+
+
+
+
+#####################################################################
+# Common Utility Functions
+
+sub _caller {
+	my $depth = 0;
+	my $call  = caller($depth);
+	while ( $call eq __PACKAGE__ ) {
+		$depth++;
+		$call = caller($depth);
+	}
+	return $call;
+}
+
+# Done in evals to avoid confusing Perl::MinimumVersion
+eval( $] >= 5.006 ? <<'END_NEW' : <<'END_OLD' ); die $@ if $@;
+sub _read {
+	local *FH;
+	open( FH, '<', $_[0] ) or die "open($_[0]): $!";
+	binmode FH;
+	my $string = do { local $/; <FH> };
+	close FH or die "close($_[0]): $!";
+	return $string;
+}
+END_NEW
+sub _read {
+	local *FH;
+	open( FH, "< $_[0]"  ) or die "open($_[0]): $!";
+	binmode FH;
+	my $string = do { local $/; <FH> };
+	close FH or die "close($_[0]): $!";
+	return $string;
+}
+END_OLD
+
+sub _readperl {
+	my $string = Module::Install::_read($_[0]);
+	$string =~ s/(?:\015{1,2}\012|\015|\012)/\n/sg;
+	$string =~ s/(\n)\n*__(?:DATA|END)__\b.*\z/$1/s;
+	$string =~ s/\n\n=\w+.+?\n\n=cut\b.+?\n+/\n\n/sg;
+	return $string;
+}
+
+sub _readpod {
+	my $string = Module::Install::_read($_[0]);
+	$string =~ s/(?:\015{1,2}\012|\015|\012)/\n/sg;
+	return $string if $_[0] =~ /\.pod\z/;
+	$string =~ s/(^|\n=cut\b.+?\n+)[^=\s].+?\n(\n=\w+|\z)/$1$2/sg;
+	$string =~ s/\n*=pod\b[^\n]*\n+/\n\n/sg;
+	$string =~ s/\n*=cut\b[^\n]*\n+/\n\n/sg;
+	$string =~ s/^\n+//s;
+	return $string;
+}
+
+# Done in evals to avoid confusing Perl::MinimumVersion
+eval( $] >= 5.006 ? <<'END_NEW' : <<'END_OLD' ); die $@ if $@;
+sub _write {
+	local *FH;
+	open( FH, '>', $_[0] ) or die "open($_[0]): $!";
+	binmode FH;
+	foreach ( 1 .. $#_ ) {
+		print FH $_[$_] or die "print($_[0]): $!";
+	}
+	close FH or die "close($_[0]): $!";
+}
+END_NEW
+sub _write {
+	local *FH;
+	open( FH, "> $_[0]"  ) or die "open($_[0]): $!";
+	binmode FH;
+	foreach ( 1 .. $#_ ) {
+		print FH $_[$_] or die "print($_[0]): $!";
+	}
+	close FH or die "close($_[0]): $!";
+}
+END_OLD
+
+# _version is for processing module versions (eg, 1.03_05) not
+# Perl versions (eg, 5.8.1).
+sub _version {
+	my $s = shift || 0;
+	my $d =()= $s =~ /(\.)/g;
+	if ( $d >= 2 ) {
+		# Normalise multipart versions
+		$s =~ s/(\.)(\d{1,3})/sprintf("$1%03d",$2)/eg;
+	}
+	$s =~ s/^(\d+)\.?//;
+	my $l = $1 || 0;
+	my @v = map {
+		$_ . '0' x (3 - length $_)
+	} $s =~ /(\d{1,3})\D?/g;
+	$l = $l . '.' . join '', @v if @v;
+	return $l + 0;
+}
+
+sub _cmp {
+	_version($_[1]) <=> _version($_[2]);
+}
+
+# Cloned from Params::Util::_CLASS
+sub _CLASS {
+	(
+		defined $_[0]
+		and
+		! ref $_[0]
+		and
+		$_[0] =~ m/^[^\W\d]\w*(?:::\w+)*\z/s
+	) ? $_[0] : undef;
+}
+
+1;
+
+# Copyright 2008 - 2012 Adam Kennedy.
diff --git a/inc/Module/Install/Base.pm b/inc/Module/Install/Base.pm
new file mode 100644
index 0000000..4206347
--- /dev/null
+++ b/inc/Module/Install/Base.pm
@@ -0,0 +1,83 @@
+#line 1
+package Module::Install::Base;
+
+use strict 'vars';
+use vars qw{$VERSION};
+BEGIN {
+	$VERSION = '1.14';
+}
+
+# Suspend handler for "redefined" warnings
+BEGIN {
+	my $w = $SIG{__WARN__};
+	$SIG{__WARN__} = sub { $w };
+}
+
+#line 42
+
+sub new {
+	my $class = shift;
+	unless ( defined &{"${class}::call"} ) {
+		*{"${class}::call"} = sub { shift->_top->call(@_) };
+	}
+	unless ( defined &{"${class}::load"} ) {
+		*{"${class}::load"} = sub { shift->_top->load(@_) };
+	}
+	bless { @_ }, $class;
+}
+
+#line 61
+
+sub AUTOLOAD {
+	local $@;
+	my $func = eval { shift->_top->autoload } or return;
+	goto &$func;
+}
+
+#line 75
+
+sub _top {
+	$_[0]->{_top};
+}
+
+#line 90
+
+sub admin {
+	$_[0]->_top->{admin}
+	or
+	Module::Install::Base::FakeAdmin->new;
+}
+
+#line 106
+
+sub is_admin {
+	! $_[0]->admin->isa('Module::Install::Base::FakeAdmin');
+}
+
+sub DESTROY {}
+
+package Module::Install::Base::FakeAdmin;
+
+use vars qw{$VERSION};
+BEGIN {
+	$VERSION = $Module::Install::Base::VERSION;
+}
+
+my $fake;
+
+sub new {
+	$fake ||= bless(\@_, $_[0]);
+}
+
+sub AUTOLOAD {}
+
+sub DESTROY {}
+
+# Restore warning handler
+BEGIN {
+	$SIG{__WARN__} = $SIG{__WARN__}->();
+}
+
+1;
+
+#line 159
diff --git a/inc/Module/Install/Can.pm b/inc/Module/Install/Can.pm
new file mode 100644
index 0000000..9929b1b
--- /dev/null
+++ b/inc/Module/Install/Can.pm
@@ -0,0 +1,154 @@
+#line 1
+package Module::Install::Can;
+
+use strict;
+use Config                ();
+use ExtUtils::MakeMaker   ();
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.14';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+# check if we can load some module
+### Upgrade this to not have to load the module if possible
+sub can_use {
+	my ($self, $mod, $ver) = @_;
+	$mod =~ s{::|\\}{/}g;
+	$mod .= '.pm' unless $mod =~ /\.pm$/i;
+
+	my $pkg = $mod;
+	$pkg =~ s{/}{::}g;
+	$pkg =~ s{\.pm$}{}i;
+
+	local $@;
+	eval { require $mod; $pkg->VERSION($ver || 0); 1 };
+}
+
+# Check if we can run some command
+sub can_run {
+	my ($self, $cmd) = @_;
+
+	my $_cmd = $cmd;
+	return $_cmd if (-x $_cmd or $_cmd = MM->maybe_command($_cmd));
+
+	for my $dir ((split /$Config::Config{path_sep}/, $ENV{PATH}), '.') {
+		next if $dir eq '';
+		require File::Spec;
+		my $abs = File::Spec->catfile($dir, $cmd);
+		return $abs if (-x $abs or $abs = MM->maybe_command($abs));
+	}
+
+	return;
+}
+
+# Can our C compiler environment build XS files
+sub can_xs {
+	my $self = shift;
+
+	# Ensure we have the CBuilder module
+	$self->configure_requires( 'ExtUtils::CBuilder' => 0.27 );
+
+	# Do we have the configure_requires checker?
+	local $@;
+	eval "require ExtUtils::CBuilder;";
+	if ( $@ ) {
+		# They don't obey configure_requires, so it is
+		# someone old and delicate. Try to avoid hurting
+		# them by falling back to an older simpler test.
+		return $self->can_cc();
+	}
+
+	# Do we have a working C compiler
+	my $builder = ExtUtils::CBuilder->new(
+		quiet => 1,
+	);
+	unless ( $builder->have_compiler ) {
+		# No working C compiler
+		return 0;
+	}
+
+	# Write a C file representative of what XS becomes
+	require File::Temp;
+	my ( $FH, $tmpfile ) = File::Temp::tempfile(
+		"compilexs-XXXXX",
+		SUFFIX => '.c',
+	);
+	binmode $FH;
+	print $FH <<'END_C';
+#include "EXTERN.h"
+#include "perl.h"
+#include "XSUB.h"
+
+int main(int argc, char **argv) {
+    return 0;
+}
+
+int boot_sanexs() {
+    return 1;
+}
+
+END_C
+	close $FH;
+
+	# Can the C compiler access the same headers XS does
+	my @libs   = ();
+	my $object = undef;
+	eval {
+		local $^W = 0;
+		$object = $builder->compile(
+			source => $tmpfile,
+		);
+		@libs = $builder->link(
+			objects     => $object,
+			module_name => 'sanexs',
+		);
+	};
+	my $result = $@ ? 0 : 1;
+
+	# Clean up all the build files
+	foreach ( $tmpfile, $object, @libs ) {
+		next unless defined $_;
+		1 while unlink;
+	}
+
+	return $result;
+}
+
+# Can we locate a (the) C compiler
+sub can_cc {
+	my $self   = shift;
+	my @chunks = split(/ /, $Config::Config{cc}) or return;
+
+	# $Config{cc} may contain args; try to find out the program part
+	while (@chunks) {
+		return $self->can_run("@chunks") || (pop(@chunks), next);
+	}
+
+	return;
+}
+
+# Fix Cygwin bug on maybe_command();
+if ( $^O eq 'cygwin' ) {
+	require ExtUtils::MM_Cygwin;
+	require ExtUtils::MM_Win32;
+	if ( ! defined(&ExtUtils::MM_Cygwin::maybe_command) ) {
+		*ExtUtils::MM_Cygwin::maybe_command = sub {
+			my ($self, $file) = @_;
+			if ($file =~ m{^/cygdrive/}i and ExtUtils::MM_Win32->can('maybe_command')) {
+				ExtUtils::MM_Win32->maybe_command($file);
+			} else {
+				ExtUtils::MM_Unix->maybe_command($file);
+			}
+		}
+	}
+}
+
+1;
+
+__END__
+
+#line 236
diff --git a/inc/Module/Install/Fetch.pm b/inc/Module/Install/Fetch.pm
new file mode 100644
index 0000000..3d8de76
--- /dev/null
+++ b/inc/Module/Install/Fetch.pm
@@ -0,0 +1,93 @@
+#line 1
+package Module::Install::Fetch;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.14';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+sub get_file {
+    my ($self, %args) = @_;
+    my ($scheme, $host, $path, $file) =
+        $args{url} =~ m|^(\w+)://([^/]+)(.+)/(.+)| or return;
+
+    if ( $scheme eq 'http' and ! eval { require LWP::Simple; 1 } ) {
+        $args{url} = $args{ftp_url}
+            or (warn("LWP support unavailable!\n"), return);
+        ($scheme, $host, $path, $file) =
+            $args{url} =~ m|^(\w+)://([^/]+)(.+)/(.+)| or return;
+    }
+
+    $|++;
+    print "Fetching '$file' from $host... ";
+
+    unless (eval { require Socket; Socket::inet_aton($host) }) {
+        warn "'$host' resolve failed!\n";
+        return;
+    }
+
+    return unless $scheme eq 'ftp' or $scheme eq 'http';
+
+    require Cwd;
+    my $dir = Cwd::getcwd();
+    chdir $args{local_dir} or return if exists $args{local_dir};
+
+    if (eval { require LWP::Simple; 1 }) {
+        LWP::Simple::mirror($args{url}, $file);
+    }
+    elsif (eval { require Net::FTP; 1 }) { eval {
+        # use Net::FTP to get past firewall
+        my $ftp = Net::FTP->new($host, Passive => 1, Timeout => 600);
+        $ftp->login("anonymous", 'anonymous at example.com');
+        $ftp->cwd($path);
+        $ftp->binary;
+        $ftp->get($file) or (warn("$!\n"), return);
+        $ftp->quit;
+    } }
+    elsif (my $ftp = $self->can_run('ftp')) { eval {
+        # no Net::FTP, fallback to ftp.exe
+        require FileHandle;
+        my $fh = FileHandle->new;
+
+        local $SIG{CHLD} = 'IGNORE';
+        unless ($fh->open("|$ftp -n")) {
+            warn "Couldn't open ftp: $!\n";
+            chdir $dir; return;
+        }
+
+        my @dialog = split(/\n/, <<"END_FTP");
+open $host
+user anonymous anonymous\@example.com
+cd $path
+binary
+get $file $file
+quit
+END_FTP
+        foreach (@dialog) { $fh->print("$_\n") }
+        $fh->close;
+    } }
+    else {
+        warn "No working 'ftp' program available!\n";
+        chdir $dir; return;
+    }
+
+    unless (-f $file) {
+        warn "Fetching failed: $@\n";
+        chdir $dir; return;
+    }
+
+    return if exists $args{size} and -s $file != $args{size};
+    system($args{run}) if exists $args{run};
+    unlink($file) if $args{remove};
+
+    print(((!exists $args{check_for} or -e $args{check_for})
+        ? "done!" : "failed! ($!)"), "\n");
+    chdir $dir; return !$?;
+}
+
+1;
diff --git a/inc/Module/Install/Makefile.pm b/inc/Module/Install/Makefile.pm
new file mode 100644
index 0000000..66993af
--- /dev/null
+++ b/inc/Module/Install/Makefile.pm
@@ -0,0 +1,418 @@
+#line 1
+package Module::Install::Makefile;
+
+use strict 'vars';
+use ExtUtils::MakeMaker   ();
+use Module::Install::Base ();
+use Fcntl qw/:flock :seek/;
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.14';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+sub Makefile { $_[0] }
+
+my %seen = ();
+
+sub prompt {
+	shift;
+
+	# Infinite loop protection
+	my @c = caller();
+	if ( ++$seen{"$c[1]|$c[2]|$_[0]"} > 3 ) {
+		die "Caught an potential prompt infinite loop ($c[1]|$c[2]|$_[0])";
+	}
+
+	# In automated testing or non-interactive session, always use defaults
+	if ( ($ENV{AUTOMATED_TESTING} or -! -t STDIN) and ! $ENV{PERL_MM_USE_DEFAULT} ) {
+		local $ENV{PERL_MM_USE_DEFAULT} = 1;
+		goto &ExtUtils::MakeMaker::prompt;
+	} else {
+		goto &ExtUtils::MakeMaker::prompt;
+	}
+}
+
+# Store a cleaned up version of the MakeMaker version,
+# since we need to behave differently in a variety of
+# ways based on the MM version.
+my $makemaker = eval $ExtUtils::MakeMaker::VERSION;
+
+# If we are passed a param, do a "newer than" comparison.
+# Otherwise, just return the MakeMaker version.
+sub makemaker {
+	( @_ < 2 or $makemaker >= eval($_[1]) ) ? $makemaker : 0
+}
+
+# Ripped from ExtUtils::MakeMaker 6.56, and slightly modified
+# as we only need to know here whether the attribute is an array
+# or a hash or something else (which may or may not be appendable).
+my %makemaker_argtype = (
+ C                  => 'ARRAY',
+ CONFIG             => 'ARRAY',
+# CONFIGURE          => 'CODE', # ignore
+ DIR                => 'ARRAY',
+ DL_FUNCS           => 'HASH',
+ DL_VARS            => 'ARRAY',
+ EXCLUDE_EXT        => 'ARRAY',
+ EXE_FILES          => 'ARRAY',
+ FUNCLIST           => 'ARRAY',
+ H                  => 'ARRAY',
+ IMPORTS            => 'HASH',
+ INCLUDE_EXT        => 'ARRAY',
+ LIBS               => 'ARRAY', # ignore ''
+ MAN1PODS           => 'HASH',
+ MAN3PODS           => 'HASH',
+ META_ADD           => 'HASH',
+ META_MERGE         => 'HASH',
+ PL_FILES           => 'HASH',
+ PM                 => 'HASH',
+ PMLIBDIRS          => 'ARRAY',
+ PMLIBPARENTDIRS    => 'ARRAY',
+ PREREQ_PM          => 'HASH',
+ CONFIGURE_REQUIRES => 'HASH',
+ SKIP               => 'ARRAY',
+ TYPEMAPS           => 'ARRAY',
+ XS                 => 'HASH',
+# VERSION            => ['version',''],  # ignore
+# _KEEP_AFTER_FLUSH  => '',
+
+ clean      => 'HASH',
+ depend     => 'HASH',
+ dist       => 'HASH',
+ dynamic_lib=> 'HASH',
+ linkext    => 'HASH',
+ macro      => 'HASH',
+ postamble  => 'HASH',
+ realclean  => 'HASH',
+ test       => 'HASH',
+ tool_autosplit => 'HASH',
+
+ # special cases where you can use makemaker_append
+ CCFLAGS   => 'APPENDABLE',
+ DEFINE    => 'APPENDABLE',
+ INC       => 'APPENDABLE',
+ LDDLFLAGS => 'APPENDABLE',
+ LDFROM    => 'APPENDABLE',
+);
+
+sub makemaker_args {
+	my ($self, %new_args) = @_;
+	my $args = ( $self->{makemaker_args} ||= {} );
+	foreach my $key (keys %new_args) {
+		if ($makemaker_argtype{$key}) {
+			if ($makemaker_argtype{$key} eq 'ARRAY') {
+				$args->{$key} = [] unless defined $args->{$key};
+				unless (ref $args->{$key} eq 'ARRAY') {
+					$args->{$key} = [$args->{$key}]
+				}
+				push @{$args->{$key}},
+					ref $new_args{$key} eq 'ARRAY'
+						? @{$new_args{$key}}
+						: $new_args{$key};
+			}
+			elsif ($makemaker_argtype{$key} eq 'HASH') {
+				$args->{$key} = {} unless defined $args->{$key};
+				foreach my $skey (keys %{ $new_args{$key} }) {
+					$args->{$key}{$skey} = $new_args{$key}{$skey};
+				}
+			}
+			elsif ($makemaker_argtype{$key} eq 'APPENDABLE') {
+				$self->makemaker_append($key => $new_args{$key});
+			}
+		}
+		else {
+			if (defined $args->{$key}) {
+				warn qq{MakeMaker attribute "$key" is overriden; use "makemaker_append" to append values\n};
+			}
+			$args->{$key} = $new_args{$key};
+		}
+	}
+	return $args;
+}
+
+# For mm args that take multiple space-separated args,
+# append an argument to the current list.
+sub makemaker_append {
+	my $self = shift;
+	my $name = shift;
+	my $args = $self->makemaker_args;
+	$args->{$name} = defined $args->{$name}
+		? join( ' ', $args->{$name}, @_ )
+		: join( ' ', @_ );
+}
+
+sub build_subdirs {
+	my $self    = shift;
+	my $subdirs = $self->makemaker_args->{DIR} ||= [];
+	for my $subdir (@_) {
+		push @$subdirs, $subdir;
+	}
+}
+
+sub clean_files {
+	my $self  = shift;
+	my $clean = $self->makemaker_args->{clean} ||= {};
+	  %$clean = (
+		%$clean,
+		FILES => join ' ', grep { length $_ } ($clean->{FILES} || (), @_),
+	);
+}
+
+sub realclean_files {
+	my $self      = shift;
+	my $realclean = $self->makemaker_args->{realclean} ||= {};
+	  %$realclean = (
+		%$realclean,
+		FILES => join ' ', grep { length $_ } ($realclean->{FILES} || (), @_),
+	);
+}
+
+sub libs {
+	my $self = shift;
+	my $libs = ref $_[0] ? shift : [ shift ];
+	$self->makemaker_args( LIBS => $libs );
+}
+
+sub inc {
+	my $self = shift;
+	$self->makemaker_args( INC => shift );
+}
+
+sub _wanted_t {
+}
+
+sub tests_recursive {
+	my $self = shift;
+	my $dir = shift || 't';
+	unless ( -d $dir ) {
+		die "tests_recursive dir '$dir' does not exist";
+	}
+	my %tests = map { $_ => 1 } split / /, ($self->tests || '');
+	require File::Find;
+	File::Find::find(
+        sub { /\.t$/ and -f $_ and $tests{"$File::Find::dir/*.t"} = 1 },
+        $dir
+    );
+	$self->tests( join ' ', sort keys %tests );
+}
+
+sub write {
+	my $self = shift;
+	die "&Makefile->write() takes no arguments\n" if @_;
+
+	# Check the current Perl version
+	my $perl_version = $self->perl_version;
+	if ( $perl_version ) {
+		eval "use $perl_version; 1"
+			or die "ERROR: perl: Version $] is installed, "
+			. "but we need version >= $perl_version";
+	}
+
+	# Make sure we have a new enough MakeMaker
+	require ExtUtils::MakeMaker;
+
+	if ( $perl_version and $self->_cmp($perl_version, '5.006') >= 0 ) {
+		# This previous attempted to inherit the version of
+		# ExtUtils::MakeMaker in use by the module author, but this
+		# was found to be untenable as some authors build releases
+		# using future dev versions of EU:MM that nobody else has.
+		# Instead, #toolchain suggests we use 6.59 which is the most
+		# stable version on CPAN at time of writing and is, to quote
+		# ribasushi, "not terminally fucked, > and tested enough".
+		# TODO: We will now need to maintain this over time to push
+		# the version up as new versions are released.
+		$self->build_requires(     'ExtUtils::MakeMaker' => 6.59 );
+		$self->configure_requires( 'ExtUtils::MakeMaker' => 6.59 );
+	} else {
+		# Allow legacy-compatibility with 5.005 by depending on the
+		# most recent EU:MM that supported 5.005.
+		$self->build_requires(     'ExtUtils::MakeMaker' => 6.36 );
+		$self->configure_requires( 'ExtUtils::MakeMaker' => 6.36 );
+	}
+
+	# Generate the MakeMaker params
+	my $args = $self->makemaker_args;
+	$args->{DISTNAME} = $self->name;
+	$args->{NAME}     = $self->module_name || $self->name;
+	$args->{NAME}     =~ s/-/::/g;
+	$args->{VERSION}  = $self->version or die <<'EOT';
+ERROR: Can't determine distribution version. Please specify it
+explicitly via 'version' in Makefile.PL, or set a valid $VERSION
+in a module, and provide its file path via 'version_from' (or
+'all_from' if you prefer) in Makefile.PL.
+EOT
+
+	if ( $self->tests ) {
+		my @tests = split ' ', $self->tests;
+		my %seen;
+		$args->{test} = {
+			TESTS => (join ' ', grep {!$seen{$_}++} @tests),
+		};
+    } elsif ( $Module::Install::ExtraTests::use_extratests ) {
+        # Module::Install::ExtraTests doesn't set $self->tests and does its own tests via harness.
+        # So, just ignore our xt tests here.
+	} elsif ( -d 'xt' and ($Module::Install::AUTHOR or $ENV{RELEASE_TESTING}) ) {
+		$args->{test} = {
+			TESTS => join( ' ', map { "$_/*.t" } grep { -d $_ } qw{ t xt } ),
+		};
+	}
+	if ( $] >= 5.005 ) {
+		$args->{ABSTRACT} = $self->abstract;
+		$args->{AUTHOR}   = join ', ', @{$self->author || []};
+	}
+	if ( $self->makemaker(6.10) ) {
+		$args->{NO_META}   = 1;
+		#$args->{NO_MYMETA} = 1;
+	}
+	if ( $self->makemaker(6.17) and $self->sign ) {
+		$args->{SIGN} = 1;
+	}
+	unless ( $self->is_admin ) {
+		delete $args->{SIGN};
+	}
+	if ( $self->makemaker(6.31) and $self->license ) {
+		$args->{LICENSE} = $self->license;
+	}
+
+	my $prereq = ($args->{PREREQ_PM} ||= {});
+	%$prereq = ( %$prereq,
+		map { @$_ } # flatten [module => version]
+		map { @$_ }
+		grep $_,
+		($self->requires)
+	);
+
+	# Remove any reference to perl, PREREQ_PM doesn't support it
+	delete $args->{PREREQ_PM}->{perl};
+
+	# Merge both kinds of requires into BUILD_REQUIRES
+	my $build_prereq = ($args->{BUILD_REQUIRES} ||= {});
+	%$build_prereq = ( %$build_prereq,
+		map { @$_ } # flatten [module => version]
+		map { @$_ }
+		grep $_,
+		($self->configure_requires, $self->build_requires)
+	);
+
+	# Remove any reference to perl, BUILD_REQUIRES doesn't support it
+	delete $args->{BUILD_REQUIRES}->{perl};
+
+	# Delete bundled dists from prereq_pm, add it to Makefile DIR
+	my $subdirs = ($args->{DIR} || []);
+	if ($self->bundles) {
+		my %processed;
+		foreach my $bundle (@{ $self->bundles }) {
+			my ($mod_name, $dist_dir) = @$bundle;
+			delete $prereq->{$mod_name};
+			$dist_dir = File::Basename::basename($dist_dir); # dir for building this module
+			if (not exists $processed{$dist_dir}) {
+				if (-d $dist_dir) {
+					# List as sub-directory to be processed by make
+					push @$subdirs, $dist_dir;
+				}
+				# Else do nothing: the module is already present on the system
+				$processed{$dist_dir} = undef;
+			}
+		}
+	}
+
+	unless ( $self->makemaker('6.55_03') ) {
+		%$prereq = (%$prereq,%$build_prereq);
+		delete $args->{BUILD_REQUIRES};
+	}
+
+	if ( my $perl_version = $self->perl_version ) {
+		eval "use $perl_version; 1"
+			or die "ERROR: perl: Version $] is installed, "
+			. "but we need version >= $perl_version";
+
+		if ( $self->makemaker(6.48) ) {
+			$args->{MIN_PERL_VERSION} = $perl_version;
+		}
+	}
+
+	if ($self->installdirs) {
+		warn qq{old INSTALLDIRS (probably set by makemaker_args) is overriden by installdirs\n} if $args->{INSTALLDIRS};
+		$args->{INSTALLDIRS} = $self->installdirs;
+	}
+
+	my %args = map {
+		( $_ => $args->{$_} ) } grep {defined($args->{$_} )
+	} keys %$args;
+
+	my $user_preop = delete $args{dist}->{PREOP};
+	if ( my $preop = $self->admin->preop($user_preop) ) {
+		foreach my $key ( keys %$preop ) {
+			$args{dist}->{$key} = $preop->{$key};
+		}
+	}
+
+	my $mm = ExtUtils::MakeMaker::WriteMakefile(%args);
+	$self->fix_up_makefile($mm->{FIRST_MAKEFILE} || 'Makefile');
+}
+
+sub fix_up_makefile {
+	my $self          = shift;
+	my $makefile_name = shift;
+	my $top_class     = ref($self->_top) || '';
+	my $top_version   = $self->_top->VERSION || '';
+
+	my $preamble = $self->preamble
+		? "# Preamble by $top_class $top_version\n"
+			. $self->preamble
+		: '';
+	my $postamble = "# Postamble by $top_class $top_version\n"
+		. ($self->postamble || '');
+
+	local *MAKEFILE;
+	open MAKEFILE, "+< $makefile_name" or die "fix_up_makefile: Couldn't open $makefile_name: $!";
+	eval { flock MAKEFILE, LOCK_EX };
+	my $makefile = do { local $/; <MAKEFILE> };
+
+	$makefile =~ s/\b(test_harness\(\$\(TEST_VERBOSE\), )/$1'inc', /;
+	$makefile =~ s/( -I\$\(INST_ARCHLIB\))/ -Iinc$1/g;
+	$makefile =~ s/( "-I\$\(INST_LIB\)")/ "-Iinc"$1/g;
+	$makefile =~ s/^(FULLPERL = .*)/$1 "-Iinc"/m;
+	$makefile =~ s/^(PERL = .*)/$1 "-Iinc"/m;
+
+	# Module::Install will never be used to build the Core Perl
+	# Sometimes PERL_LIB and PERL_ARCHLIB get written anyway, which breaks
+	# PREFIX/PERL5LIB, and thus, install_share. Blank them if they exist
+	$makefile =~ s/^PERL_LIB = .+/PERL_LIB =/m;
+	#$makefile =~ s/^PERL_ARCHLIB = .+/PERL_ARCHLIB =/m;
+
+	# Perl 5.005 mentions PERL_LIB explicitly, so we have to remove that as well.
+	$makefile =~ s/(\"?)-I\$\(PERL_LIB\)\1//g;
+
+	# XXX - This is currently unused; not sure if it breaks other MM-users
+	# $makefile =~ s/^pm_to_blib\s+:\s+/pm_to_blib :: /mg;
+
+	seek MAKEFILE, 0, SEEK_SET;
+	truncate MAKEFILE, 0;
+	print MAKEFILE  "$preamble$makefile$postamble" or die $!;
+	close MAKEFILE  or die $!;
+
+	1;
+}
+
+sub preamble {
+	my ($self, $text) = @_;
+	$self->{preamble} = $text . $self->{preamble} if defined $text;
+	$self->{preamble};
+}
+
+sub postamble {
+	my ($self, $text) = @_;
+	$self->{postamble} ||= $self->admin->postamble;
+	$self->{postamble} .= $text if defined $text;
+	$self->{postamble}
+}
+
+1;
+
+__END__
+
+#line 544
diff --git a/inc/Module/Install/Metadata.pm b/inc/Module/Install/Metadata.pm
new file mode 100644
index 0000000..e547fa0
--- /dev/null
+++ b/inc/Module/Install/Metadata.pm
@@ -0,0 +1,722 @@
+#line 1
+package Module::Install::Metadata;
+
+use strict 'vars';
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.14';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+my @boolean_keys = qw{
+	sign
+};
+
+my @scalar_keys = qw{
+	name
+	module_name
+	abstract
+	version
+	distribution_type
+	tests
+	installdirs
+};
+
+my @tuple_keys = qw{
+	configure_requires
+	build_requires
+	requires
+	recommends
+	bundles
+	resources
+};
+
+my @resource_keys = qw{
+	homepage
+	bugtracker
+	repository
+};
+
+my @array_keys = qw{
+	keywords
+	author
+};
+
+*authors = \&author;
+
+sub Meta              { shift          }
+sub Meta_BooleanKeys  { @boolean_keys  }
+sub Meta_ScalarKeys   { @scalar_keys   }
+sub Meta_TupleKeys    { @tuple_keys    }
+sub Meta_ResourceKeys { @resource_keys }
+sub Meta_ArrayKeys    { @array_keys    }
+
+foreach my $key ( @boolean_keys ) {
+	*$key = sub {
+		my $self = shift;
+		if ( defined wantarray and not @_ ) {
+			return $self->{values}->{$key};
+		}
+		$self->{values}->{$key} = ( @_ ? $_[0] : 1 );
+		return $self;
+	};
+}
+
+foreach my $key ( @scalar_keys ) {
+	*$key = sub {
+		my $self = shift;
+		return $self->{values}->{$key} if defined wantarray and !@_;
+		$self->{values}->{$key} = shift;
+		return $self;
+	};
+}
+
+foreach my $key ( @array_keys ) {
+	*$key = sub {
+		my $self = shift;
+		return $self->{values}->{$key} if defined wantarray and !@_;
+		$self->{values}->{$key} ||= [];
+		push @{$self->{values}->{$key}}, @_;
+		return $self;
+	};
+}
+
+foreach my $key ( @resource_keys ) {
+	*$key = sub {
+		my $self = shift;
+		unless ( @_ ) {
+			return () unless $self->{values}->{resources};
+			return map  { $_->[1] }
+			       grep { $_->[0] eq $key }
+			       @{ $self->{values}->{resources} };
+		}
+		return $self->{values}->{resources}->{$key} unless @_;
+		my $uri = shift or die(
+			"Did not provide a value to $key()"
+		);
+		$self->resources( $key => $uri );
+		return 1;
+	};
+}
+
+foreach my $key ( grep { $_ ne "resources" } @tuple_keys) {
+	*$key = sub {
+		my $self = shift;
+		return $self->{values}->{$key} unless @_;
+		my @added;
+		while ( @_ ) {
+			my $module  = shift or last;
+			my $version = shift || 0;
+			push @added, [ $module, $version ];
+		}
+		push @{ $self->{values}->{$key} }, @added;
+		return map {@$_} @added;
+	};
+}
+
+# Resource handling
+my %lc_resource = map { $_ => 1 } qw{
+	homepage
+	license
+	bugtracker
+	repository
+};
+
+sub resources {
+	my $self = shift;
+	while ( @_ ) {
+		my $name  = shift or last;
+		my $value = shift or next;
+		if ( $name eq lc $name and ! $lc_resource{$name} ) {
+			die("Unsupported reserved lowercase resource '$name'");
+		}
+		$self->{values}->{resources} ||= [];
+		push @{ $self->{values}->{resources} }, [ $name, $value ];
+	}
+	$self->{values}->{resources};
+}
+
+# Aliases for build_requires that will have alternative
+# meanings in some future version of META.yml.
+sub test_requires     { shift->build_requires(@_) }
+sub install_requires  { shift->build_requires(@_) }
+
+# Aliases for installdirs options
+sub install_as_core   { $_[0]->installdirs('perl')   }
+sub install_as_cpan   { $_[0]->installdirs('site')   }
+sub install_as_site   { $_[0]->installdirs('site')   }
+sub install_as_vendor { $_[0]->installdirs('vendor') }
+
+sub dynamic_config {
+	my $self  = shift;
+	my $value = @_ ? shift : 1;
+	if ( $self->{values}->{dynamic_config} ) {
+		# Once dynamic we never change to static, for safety
+		return 0;
+	}
+	$self->{values}->{dynamic_config} = $value ? 1 : 0;
+	return 1;
+}
+
+# Convenience command
+sub static_config {
+	shift->dynamic_config(0);
+}
+
+sub perl_version {
+	my $self = shift;
+	return $self->{values}->{perl_version} unless @_;
+	my $version = shift or die(
+		"Did not provide a value to perl_version()"
+	);
+
+	# Normalize the version
+	$version = $self->_perl_version($version);
+
+	# We don't support the really old versions
+	unless ( $version >= 5.005 ) {
+		die "Module::Install only supports 5.005 or newer (use ExtUtils::MakeMaker)\n";
+	}
+
+	$self->{values}->{perl_version} = $version;
+}
+
+sub all_from {
+	my ( $self, $file ) = @_;
+
+	unless ( defined($file) ) {
+		my $name = $self->name or die(
+			"all_from called with no args without setting name() first"
+		);
+		$file = join('/', 'lib', split(/-/, $name)) . '.pm';
+		$file =~ s{.*/}{} unless -e $file;
+		unless ( -e $file ) {
+			die("all_from cannot find $file from $name");
+		}
+	}
+	unless ( -f $file ) {
+		die("The path '$file' does not exist, or is not a file");
+	}
+
+	$self->{values}{all_from} = $file;
+
+	# Some methods pull from POD instead of code.
+	# If there is a matching .pod, use that instead
+	my $pod = $file;
+	$pod =~ s/\.pm$/.pod/i;
+	$pod = $file unless -e $pod;
+
+	# Pull the different values
+	$self->name_from($file)         unless $self->name;
+	$self->version_from($file)      unless $self->version;
+	$self->perl_version_from($file) unless $self->perl_version;
+	$self->author_from($pod)        unless @{$self->author || []};
+	$self->license_from($pod)       unless $self->license;
+	$self->abstract_from($pod)      unless $self->abstract;
+
+	return 1;
+}
+
+sub provides {
+	my $self     = shift;
+	my $provides = ( $self->{values}->{provides} ||= {} );
+	%$provides = (%$provides, @_) if @_;
+	return $provides;
+}
+
+sub auto_provides {
+	my $self = shift;
+	return $self unless $self->is_admin;
+	unless (-e 'MANIFEST') {
+		warn "Cannot deduce auto_provides without a MANIFEST, skipping\n";
+		return $self;
+	}
+	# Avoid spurious warnings as we are not checking manifest here.
+	local $SIG{__WARN__} = sub {1};
+	require ExtUtils::Manifest;
+	local *ExtUtils::Manifest::manicheck = sub { return };
+
+	require Module::Build;
+	my $build = Module::Build->new(
+		dist_name    => $self->name,
+		dist_version => $self->version,
+		license      => $self->license,
+	);
+	$self->provides( %{ $build->find_dist_packages || {} } );
+}
+
+sub feature {
+	my $self     = shift;
+	my $name     = shift;
+	my $features = ( $self->{values}->{features} ||= [] );
+	my $mods;
+
+	if ( @_ == 1 and ref( $_[0] ) ) {
+		# The user used ->feature like ->features by passing in the second
+		# argument as a reference.  Accomodate for that.
+		$mods = $_[0];
+	} else {
+		$mods = \@_;
+	}
+
+	my $count = 0;
+	push @$features, (
+		$name => [
+			map {
+				ref($_) ? ( ref($_) eq 'HASH' ) ? %$_ : @$_ : $_
+			} @$mods
+		]
+	);
+
+	return @$features;
+}
+
+sub features {
+	my $self = shift;
+	while ( my ( $name, $mods ) = splice( @_, 0, 2 ) ) {
+		$self->feature( $name, @$mods );
+	}
+	return $self->{values}->{features}
+		? @{ $self->{values}->{features} }
+		: ();
+}
+
+sub no_index {
+	my $self = shift;
+	my $type = shift;
+	push @{ $self->{values}->{no_index}->{$type} }, @_ if $type;
+	return $self->{values}->{no_index};
+}
+
+sub read {
+	my $self = shift;
+	$self->include_deps( 'YAML::Tiny', 0 );
+
+	require YAML::Tiny;
+	my $data = YAML::Tiny::LoadFile('META.yml');
+
+	# Call methods explicitly in case user has already set some values.
+	while ( my ( $key, $value ) = each %$data ) {
+		next unless $self->can($key);
+		if ( ref $value eq 'HASH' ) {
+			while ( my ( $module, $version ) = each %$value ) {
+				$self->can($key)->($self, $module => $version );
+			}
+		} else {
+			$self->can($key)->($self, $value);
+		}
+	}
+	return $self;
+}
+
+sub write {
+	my $self = shift;
+	return $self unless $self->is_admin;
+	$self->admin->write_meta;
+	return $self;
+}
+
+sub version_from {
+	require ExtUtils::MM_Unix;
+	my ( $self, $file ) = @_;
+	$self->version( ExtUtils::MM_Unix->parse_version($file) );
+
+	# for version integrity check
+	$self->makemaker_args( VERSION_FROM => $file );
+}
+
+sub abstract_from {
+	require ExtUtils::MM_Unix;
+	my ( $self, $file ) = @_;
+	$self->abstract(
+		bless(
+			{ DISTNAME => $self->name },
+			'ExtUtils::MM_Unix'
+		)->parse_abstract($file)
+	);
+}
+
+# Add both distribution and module name
+sub name_from {
+	my ($self, $file) = @_;
+	if (
+		Module::Install::_read($file) =~ m/
+		^ \s*
+		package \s*
+		([\w:]+)
+		[\s|;]*
+		/ixms
+	) {
+		my ($name, $module_name) = ($1, $1);
+		$name =~ s{::}{-}g;
+		$self->name($name);
+		unless ( $self->module_name ) {
+			$self->module_name($module_name);
+		}
+	} else {
+		die("Cannot determine name from $file\n");
+	}
+}
+
+sub _extract_perl_version {
+	if (
+		$_[0] =~ m/
+		^\s*
+		(?:use|require) \s*
+		v?
+		([\d_\.]+)
+		\s* ;
+		/ixms
+	) {
+		my $perl_version = $1;
+		$perl_version =~ s{_}{}g;
+		return $perl_version;
+	} else {
+		return;
+	}
+}
+
+sub perl_version_from {
+	my $self = shift;
+	my $perl_version=_extract_perl_version(Module::Install::_read($_[0]));
+	if ($perl_version) {
+		$self->perl_version($perl_version);
+	} else {
+		warn "Cannot determine perl version info from $_[0]\n";
+		return;
+	}
+}
+
+sub author_from {
+	my $self    = shift;
+	my $content = Module::Install::_read($_[0]);
+	if ($content =~ m/
+		=head \d \s+ (?:authors?)\b \s*
+		([^\n]*)
+		|
+		=head \d \s+ (?:licen[cs]e|licensing|copyright|legal)\b \s*
+		.*? copyright .*? \d\d\d[\d.]+ \s* (?:\bby\b)? \s*
+		([^\n]*)
+	/ixms) {
+		my $author = $1 || $2;
+
+		# XXX: ugly but should work anyway...
+		if (eval "require Pod::Escapes; 1") {
+			# Pod::Escapes has a mapping table.
+			# It's in core of perl >= 5.9.3, and should be installed
+			# as one of the Pod::Simple's prereqs, which is a prereq
+			# of Pod::Text 3.x (see also below).
+			$author =~ s{ E<( (\d+) | ([A-Za-z]+) )> }
+			{
+				defined $2
+				? chr($2)
+				: defined $Pod::Escapes::Name2character_number{$1}
+				? chr($Pod::Escapes::Name2character_number{$1})
+				: do {
+					warn "Unknown escape: E<$1>";
+					"E<$1>";
+				};
+			}gex;
+		}
+		elsif (eval "require Pod::Text; 1" && $Pod::Text::VERSION < 3) {
+			# Pod::Text < 3.0 has yet another mapping table,
+			# though the table name of 2.x and 1.x are different.
+			# (1.x is in core of Perl < 5.6, 2.x is in core of
+			# Perl < 5.9.3)
+			my $mapping = ($Pod::Text::VERSION < 2)
+				? \%Pod::Text::HTML_Escapes
+				: \%Pod::Text::ESCAPES;
+			$author =~ s{ E<( (\d+) | ([A-Za-z]+) )> }
+			{
+				defined $2
+				? chr($2)
+				: defined $mapping->{$1}
+				? $mapping->{$1}
+				: do {
+					warn "Unknown escape: E<$1>";
+					"E<$1>";
+				};
+			}gex;
+		}
+		else {
+			$author =~ s{E<lt>}{<}g;
+			$author =~ s{E<gt>}{>}g;
+		}
+		$self->author($author);
+	} else {
+		warn "Cannot determine author info from $_[0]\n";
+	}
+}
+
+#Stolen from M::B
+my %license_urls = (
+    perl         => 'http://dev.perl.org/licenses/',
+    apache       => 'http://apache.org/licenses/LICENSE-2.0',
+    apache_1_1   => 'http://apache.org/licenses/LICENSE-1.1',
+    artistic     => 'http://opensource.org/licenses/artistic-license.php',
+    artistic_2   => 'http://opensource.org/licenses/artistic-license-2.0.php',
+    lgpl         => 'http://opensource.org/licenses/lgpl-license.php',
+    lgpl2        => 'http://opensource.org/licenses/lgpl-2.1.php',
+    lgpl3        => 'http://opensource.org/licenses/lgpl-3.0.html',
+    bsd          => 'http://opensource.org/licenses/bsd-license.php',
+    gpl          => 'http://opensource.org/licenses/gpl-license.php',
+    gpl2         => 'http://opensource.org/licenses/gpl-2.0.php',
+    gpl3         => 'http://opensource.org/licenses/gpl-3.0.html',
+    mit          => 'http://opensource.org/licenses/mit-license.php',
+    mozilla      => 'http://opensource.org/licenses/mozilla1.1.php',
+    open_source  => undef,
+    unrestricted => undef,
+    restrictive  => undef,
+    unknown      => undef,
+);
+
+sub license {
+	my $self = shift;
+	return $self->{values}->{license} unless @_;
+	my $license = shift or die(
+		'Did not provide a value to license()'
+	);
+	$license = __extract_license($license) || lc $license;
+	$self->{values}->{license} = $license;
+
+	# Automatically fill in license URLs
+	if ( $license_urls{$license} ) {
+		$self->resources( license => $license_urls{$license} );
+	}
+
+	return 1;
+}
+
+sub _extract_license {
+	my $pod = shift;
+	my $matched;
+	return __extract_license(
+		($matched) = $pod =~ m/
+			(=head \d \s+ L(?i:ICEN[CS]E|ICENSING)\b.*?)
+			(=head \d.*|=cut.*|)\z
+		/xms
+	) || __extract_license(
+		($matched) = $pod =~ m/
+			(=head \d \s+ (?:C(?i:OPYRIGHTS?)|L(?i:EGAL))\b.*?)
+			(=head \d.*|=cut.*|)\z
+		/xms
+	);
+}
+
+sub __extract_license {
+	my $license_text = shift or return;
+	my @phrases      = (
+		'(?:under )?the same (?:terms|license) as (?:perl|the perl (?:\d )?programming language)' => 'perl', 1,
+		'(?:under )?the terms of (?:perl|the perl programming language) itself' => 'perl', 1,
+		'Artistic and GPL'                   => 'perl',         1,
+		'GNU general public license'         => 'gpl',          1,
+		'GNU public license'                 => 'gpl',          1,
+		'GNU lesser general public license'  => 'lgpl',         1,
+		'GNU lesser public license'          => 'lgpl',         1,
+		'GNU library general public license' => 'lgpl',         1,
+		'GNU library public license'         => 'lgpl',         1,
+		'GNU Free Documentation license'     => 'unrestricted', 1,
+		'GNU Affero General Public License'  => 'open_source',  1,
+		'(?:Free)?BSD license'               => 'bsd',          1,
+		'Artistic license 2\.0'              => 'artistic_2',   1,
+		'Artistic license'                   => 'artistic',     1,
+		'Apache (?:Software )?license'       => 'apache',       1,
+		'GPL'                                => 'gpl',          1,
+		'LGPL'                               => 'lgpl',         1,
+		'BSD'                                => 'bsd',          1,
+		'Artistic'                           => 'artistic',     1,
+		'MIT'                                => 'mit',          1,
+		'Mozilla Public License'             => 'mozilla',      1,
+		'Q Public License'                   => 'open_source',  1,
+		'OpenSSL License'                    => 'unrestricted', 1,
+		'SSLeay License'                     => 'unrestricted', 1,
+		'zlib License'                       => 'open_source',  1,
+		'proprietary'                        => 'proprietary',  0,
+	);
+	while ( my ($pattern, $license, $osi) = splice(@phrases, 0, 3) ) {
+		$pattern =~ s#\s+#\\s+#gs;
+		if ( $license_text =~ /\b$pattern\b/i ) {
+			return $license;
+		}
+	}
+	return '';
+}
+
+sub license_from {
+	my $self = shift;
+	if (my $license=_extract_license(Module::Install::_read($_[0]))) {
+		$self->license($license);
+	} else {
+		warn "Cannot determine license info from $_[0]\n";
+		return 'unknown';
+	}
+}
+
+sub _extract_bugtracker {
+	my @links   = $_[0] =~ m#L<(
+	 https?\Q://rt.cpan.org/\E[^>]+|
+	 https?\Q://github.com/\E[\w_]+/[\w_]+/issues|
+	 https?\Q://code.google.com/p/\E[\w_\-]+/issues/list
+	 )>#gx;
+	my %links;
+	@links{@links}=();
+	@links=keys %links;
+	return @links;
+}
+
+sub bugtracker_from {
+	my $self    = shift;
+	my $content = Module::Install::_read($_[0]);
+	my @links   = _extract_bugtracker($content);
+	unless ( @links ) {
+		warn "Cannot determine bugtracker info from $_[0]\n";
+		return 0;
+	}
+	if ( @links > 1 ) {
+		warn "Found more than one bugtracker link in $_[0]\n";
+		return 0;
+	}
+
+	# Set the bugtracker
+	bugtracker( $links[0] );
+	return 1;
+}
+
+sub requires_from {
+	my $self     = shift;
+	my $content  = Module::Install::_readperl($_[0]);
+	my @requires = $content =~ m/^use\s+([^\W\d]\w*(?:::\w+)*)\s+(v?[\d\.]+)/mg;
+	while ( @requires ) {
+		my $module  = shift @requires;
+		my $version = shift @requires;
+		$self->requires( $module => $version );
+	}
+}
+
+sub test_requires_from {
+	my $self     = shift;
+	my $content  = Module::Install::_readperl($_[0]);
+	my @requires = $content =~ m/^use\s+([^\W\d]\w*(?:::\w+)*)\s+([\d\.]+)/mg;
+	while ( @requires ) {
+		my $module  = shift @requires;
+		my $version = shift @requires;
+		$self->test_requires( $module => $version );
+	}
+}
+
+# Convert triple-part versions (eg, 5.6.1 or 5.8.9) to
+# numbers (eg, 5.006001 or 5.008009).
+# Also, convert double-part versions (eg, 5.8)
+sub _perl_version {
+	my $v = $_[-1];
+	$v =~ s/^([1-9])\.([1-9]\d?\d?)$/sprintf("%d.%03d",$1,$2)/e;
+	$v =~ s/^([1-9])\.([1-9]\d?\d?)\.(0|[1-9]\d?\d?)$/sprintf("%d.%03d%03d",$1,$2,$3 || 0)/e;
+	$v =~ s/(\.\d\d\d)000$/$1/;
+	$v =~ s/_.+$//;
+	if ( ref($v) ) {
+		# Numify
+		$v = $v + 0;
+	}
+	return $v;
+}
+
+sub add_metadata {
+    my $self = shift;
+    my %hash = @_;
+    for my $key (keys %hash) {
+        warn "add_metadata: $key is not prefixed with 'x_'.\n" .
+             "Use appopriate function to add non-private metadata.\n" unless $key =~ /^x_/;
+        $self->{values}->{$key} = $hash{$key};
+    }
+}
+
+
+######################################################################
+# MYMETA Support
+
+sub WriteMyMeta {
+	die "WriteMyMeta has been deprecated";
+}
+
+sub write_mymeta_yaml {
+	my $self = shift;
+
+	# We need YAML::Tiny to write the MYMETA.yml file
+	unless ( eval { require YAML::Tiny; 1; } ) {
+		return 1;
+	}
+
+	# Generate the data
+	my $meta = $self->_write_mymeta_data or return 1;
+
+	# Save as the MYMETA.yml file
+	print "Writing MYMETA.yml\n";
+	YAML::Tiny::DumpFile('MYMETA.yml', $meta);
+}
+
+sub write_mymeta_json {
+	my $self = shift;
+
+	# We need JSON to write the MYMETA.json file
+	unless ( eval { require JSON; 1; } ) {
+		return 1;
+	}
+
+	# Generate the data
+	my $meta = $self->_write_mymeta_data or return 1;
+
+	# Save as the MYMETA.yml file
+	print "Writing MYMETA.json\n";
+	Module::Install::_write(
+		'MYMETA.json',
+		JSON->new->pretty(1)->canonical->encode($meta),
+	);
+}
+
+sub _write_mymeta_data {
+	my $self = shift;
+
+	# If there's no existing META.yml there is nothing we can do
+	return undef unless -f 'META.yml';
+
+	# We need Parse::CPAN::Meta to load the file
+	unless ( eval { require Parse::CPAN::Meta; 1; } ) {
+		return undef;
+	}
+
+	# Merge the perl version into the dependencies
+	my $val  = $self->Meta->{values};
+	my $perl = delete $val->{perl_version};
+	if ( $perl ) {
+		$val->{requires} ||= [];
+		my $requires = $val->{requires};
+
+		# Canonize to three-dot version after Perl 5.6
+		if ( $perl >= 5.006 ) {
+			$perl =~ s{^(\d+)\.(\d\d\d)(\d*)}{join('.', $1, int($2||0), int($3||0))}e
+		}
+		unshift @$requires, [ perl => $perl ];
+	}
+
+	# Load the advisory META.yml file
+	my @yaml = Parse::CPAN::Meta::LoadFile('META.yml');
+	my $meta = $yaml[0];
+
+	# Overwrite the non-configure dependency hashes
+	delete $meta->{requires};
+	delete $meta->{build_requires};
+	delete $meta->{recommends};
+	if ( exists $val->{requires} ) {
+		$meta->{requires} = { map { @$_ } @{ $val->{requires} } };
+	}
+	if ( exists $val->{build_requires} ) {
+		$meta->{build_requires} = { map { @$_ } @{ $val->{build_requires} } };
+	}
+
+	return $meta;
+}
+
+1;
diff --git a/inc/Module/Install/Scripts.pm b/inc/Module/Install/Scripts.pm
new file mode 100644
index 0000000..fba56ae
--- /dev/null
+++ b/inc/Module/Install/Scripts.pm
@@ -0,0 +1,29 @@
+#line 1
+package Module::Install::Scripts;
+
+use strict 'vars';
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.14';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+sub install_script {
+	my $self = shift;
+	my $args = $self->makemaker_args;
+	my $exe  = $args->{EXE_FILES} ||= [];
+        foreach ( @_ ) {
+		if ( -f $_ ) {
+			push @$exe, $_;
+		} elsif ( -d 'script' and -f "script/$_" ) {
+			push @$exe, "script/$_";
+		} else {
+			die("Cannot find script '$_'");
+		}
+	}
+}
+
+1;
diff --git a/inc/Module/Install/Share.pm b/inc/Module/Install/Share.pm
new file mode 100644
index 0000000..74a2d2c
--- /dev/null
+++ b/inc/Module/Install/Share.pm
@@ -0,0 +1,96 @@
+#line 1
+package Module::Install::Share;
+
+use strict;
+use Module::Install::Base ();
+use File::Find ();
+use ExtUtils::Manifest ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.14';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+sub install_share {
+	my $self = shift;
+	my $dir  = @_ ? pop   : 'share';
+	my $type = @_ ? shift : 'dist';
+	unless ( defined $type and $type eq 'module' or $type eq 'dist' ) {
+		die "Illegal or invalid share dir type '$type'";
+	}
+	unless ( defined $dir and -d $dir ) {
+    		require Carp;
+		Carp::croak("Illegal or missing directory install_share param: '$dir'");
+	}
+
+	# Split by type
+	my $S = ($^O eq 'MSWin32') ? "\\" : "\/";
+
+	my $root;
+	if ( $type eq 'dist' ) {
+		die "Too many parameters to install_share" if @_;
+
+		# Set up the install
+		$root = "\$(INST_LIB)${S}auto${S}share${S}dist${S}\$(DISTNAME)";
+	} else {
+		my $module = Module::Install::_CLASS($_[0]);
+		unless ( defined $module ) {
+			die "Missing or invalid module name '$_[0]'";
+		}
+		$module =~ s/::/-/g;
+
+		$root = "\$(INST_LIB)${S}auto${S}share${S}module${S}$module";
+	}
+
+	my $manifest = -r 'MANIFEST' ? ExtUtils::Manifest::maniread() : undef;
+	my $skip_checker = $ExtUtils::Manifest::VERSION >= 1.54
+		? ExtUtils::Manifest::maniskip()
+		: ExtUtils::Manifest::_maniskip();
+	my $postamble = '';
+	my $perm_dir = eval($ExtUtils::MakeMaker::VERSION) >= 6.52 ? '$(PERM_DIR)' : 755;
+	File::Find::find({
+		no_chdir => 1,
+		wanted => sub {
+			my $path = File::Spec->abs2rel($_, $dir);
+			if (-d $_) {
+				return if $skip_checker->($File::Find::name);
+				$postamble .=<<"END";
+\t\$(NOECHO) \$(MKPATH) "$root${S}$path"
+\t\$(NOECHO) \$(CHMOD) $perm_dir "$root${S}$path"
+END
+			}
+			else {
+				return if ref $manifest
+						&& !exists $manifest->{$File::Find::name};
+				return if $skip_checker->($File::Find::name);
+				$postamble .=<<"END";
+\t\$(NOECHO) \$(CP) "$dir${S}$path" "$root${S}$path"
+END
+			}
+		},
+	}, $dir);
+
+	# Set up the install
+	$self->postamble(<<"END_MAKEFILE");
+config ::
+$postamble
+
+END_MAKEFILE
+
+	# The above appears to behave incorrectly when used with old versions
+	# of ExtUtils::Install (known-bad on RHEL 3, with 5.8.0)
+	# So when we need to install a share directory, make sure we add a
+	# dependency on a moderately new version of ExtUtils::MakeMaker.
+	$self->build_requires( 'ExtUtils::MakeMaker' => '6.11' );
+
+	# 99% of the time we don't want to index a shared dir
+	$self->no_index( directory => $dir );
+}
+
+1;
+
+__END__
+
+#line 154
diff --git a/inc/Module/Install/Win32.pm b/inc/Module/Install/Win32.pm
new file mode 100644
index 0000000..9706e5f
--- /dev/null
+++ b/inc/Module/Install/Win32.pm
@@ -0,0 +1,64 @@
+#line 1
+package Module::Install::Win32;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.14';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+# determine if the user needs nmake, and download it if needed
+sub check_nmake {
+	my $self = shift;
+	$self->load('can_run');
+	$self->load('get_file');
+
+	require Config;
+	return unless (
+		$^O eq 'MSWin32'                     and
+		$Config::Config{make}                and
+		$Config::Config{make} =~ /^nmake\b/i and
+		! $self->can_run('nmake')
+	);
+
+	print "The required 'nmake' executable not found, fetching it...\n";
+
+	require File::Basename;
+	my $rv = $self->get_file(
+		url       => 'http://download.microsoft.com/download/vc15/Patch/1.52/W95/EN-US/Nmake15.exe',
+		ftp_url   => 'ftp://ftp.microsoft.com/Softlib/MSLFILES/Nmake15.exe',
+		local_dir => File::Basename::dirname($^X),
+		size      => 51928,
+		run       => 'Nmake15.exe /o > nul',
+		check_for => 'Nmake.exe',
+		remove    => 1,
+	);
+
+	die <<'END_MESSAGE' unless $rv;
+
+-------------------------------------------------------------------------------
+
+Since you are using Microsoft Windows, you will need the 'nmake' utility
+before installation. It's available at:
+
+  http://download.microsoft.com/download/vc15/Patch/1.52/W95/EN-US/Nmake15.exe
+      or
+  ftp://ftp.microsoft.com/Softlib/MSLFILES/Nmake15.exe
+
+Please download the file manually, save it to a directory in %PATH% (e.g.
+C:\WINDOWS\COMMAND\), then launch the MS-DOS command line shell, "cd" to
+that directory, and run "Nmake15.exe" from there; that will create the
+'nmake.exe' file needed by this module.
+
+You may then resume the installation process described in README.
+
+-------------------------------------------------------------------------------
+END_MESSAGE
+
+}
+
+1;
diff --git a/inc/Module/Install/WriteAll.pm b/inc/Module/Install/WriteAll.pm
new file mode 100644
index 0000000..dbedc00
--- /dev/null
+++ b/inc/Module/Install/WriteAll.pm
@@ -0,0 +1,63 @@
+#line 1
+package Module::Install::WriteAll;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.14';
+	@ISA     = qw{Module::Install::Base};
+	$ISCORE  = 1;
+}
+
+sub WriteAll {
+	my $self = shift;
+	my %args = (
+		meta        => 1,
+		sign        => 0,
+		inline      => 0,
+		check_nmake => 1,
+		@_,
+	);
+
+	$self->sign(1)                if $args{sign};
+	$self->admin->WriteAll(%args) if $self->is_admin;
+
+	$self->check_nmake if $args{check_nmake};
+	unless ( $self->makemaker_args->{PL_FILES} ) {
+		# XXX: This still may be a bit over-defensive...
+		unless ($self->makemaker(6.25)) {
+			$self->makemaker_args( PL_FILES => {} ) if -f 'Build.PL';
+		}
+	}
+
+	# Until ExtUtils::MakeMaker support MYMETA.yml, make sure
+	# we clean it up properly ourself.
+	$self->realclean_files('MYMETA.yml');
+
+	if ( $args{inline} ) {
+		$self->Inline->write;
+	} else {
+		$self->Makefile->write;
+	}
+
+	# The Makefile write process adds a couple of dependencies,
+	# so write the META.yml files after the Makefile.
+	if ( $args{meta} ) {
+		$self->Meta->write;
+	}
+
+	# Experimental support for MYMETA
+	if ( $ENV{X_MYMETA} ) {
+		if ( $ENV{X_MYMETA} eq 'JSON' ) {
+			$self->Meta->write_mymeta_json;
+		} else {
+			$self->Meta->write_mymeta_yaml;
+		}
+	}
+
+	return 1;
+}
+
+1;
diff --git a/lib/Zonemaster/WebBackend.pm b/lib/Zonemaster/WebBackend.pm
new file mode 100644
index 0000000..6ddb686
--- /dev/null
+++ b/lib/Zonemaster/WebBackend.pm
@@ -0,0 +1,9 @@
+package Zonemaster::WebBackend;
+
+our $VERSION = '1.1.0';
+
+use strict;
+use warnings;
+use 5.14.2;
+
+1;
diff --git a/lib/Zonemaster/WebBackend/Config.pm b/lib/Zonemaster/WebBackend/Config.pm
index 1844526..6b13a35 100644
--- a/lib/Zonemaster/WebBackend/Config.pm
+++ b/lib/Zonemaster/WebBackend/Config.pm
@@ -1,5 +1,5 @@
 package Zonemaster::WebBackend::Config;
-our $VERSION = '1.0.5';
+our $VERSION = '1.1.0';
 
 use strict;
 use warnings;
@@ -107,16 +107,22 @@ sub MaxZonemasterExecutionTime {
     return $cfg->val( 'ZONEMASTER', 'max_zonemaster_execution_time' );
 }
 
-sub NumberOfProfessesForFrontendTesting {
+sub NumberOfProcessesForFrontendTesting {
     my $cfg = _load_config();
 
-    return $cfg->val( 'ZONEMASTER', 'number_of_professes_for_frontend_testing' );
+    my $nb = $cfg->val( 'ZONEMASTER', 'number_of_professes_for_frontend_testing' );
+    $nb = $cfg->val( 'ZONEMASTER', 'number_of_processes_for_frontend_testing' ) unless ($nb);
+    
+    return $nb;
 }
 
-sub NumberOfProfessesForBatchTesting {
+sub NumberOfProcessesForBatchTesting {
     my $cfg = _load_config();
 
-    return $cfg->val( 'ZONEMASTER', 'number_of_professes_for_batch_testing' );
+    my $nb = $cfg->val( 'ZONEMASTER', 'number_of_professes_for_batch_testing' );
+    $nb = $cfg->val( 'ZONEMASTER', 'number_of_processes_for_batch_testing' ) unless ($nb);
+    
+    return $nb;
 }
 
 sub Maxmind_ISP_DB_File {
@@ -142,7 +148,27 @@ sub force_hash_id_use_in_API_starting_from_id {
 sub CustomProfilesPath {
     my $cfg = _load_config();
 
-    return $cfg->val( 'ZONEMASTER', 'cutom_profiles_path' );
+    my $value  = $cfg->val( 'ZONEMASTER', 'cutom_profiles_path' );
+    $value  = $cfg->val( 'ZONEMASTER', 'custom_profiles_path' ) unless ($value);
+    return $value;
 }
 
+sub GetCustomConfigParameter {
+	my ($slef, $section, $param_name) = @_;
+	
+    my $cfg = _load_config();
+
+    return $cfg->val( $section, $param_name );
+}
+
+sub lock_on_queue {
+    my $cfg = _load_config();
+
+    my $val = $cfg->val( 'ZONEMASTER', 'lock_on_queue' );
+
+    return $val;
+}
+
+
+
 1;
diff --git a/lib/Zonemaster/WebBackend/DB.pm b/lib/Zonemaster/WebBackend/DB.pm
index 7b4ac48..35b7691 100644
--- a/lib/Zonemaster/WebBackend/DB.pm
+++ b/lib/Zonemaster/WebBackend/DB.pm
@@ -1,6 +1,6 @@
 package Zonemaster::WebBackend::DB;
 
-our $VERSION = '1.0.5';
+our $VERSION = '1.1.0';
 
 use Moose::Role;
 
@@ -9,7 +9,7 @@ use 5.14.2;
 use Data::Dumper;
 
 requires 'add_api_user_to_db', 'user_exists_in_db', 'user_authorized', 'test_progress', 'test_results',
-  'create_new_batch_job', 'create_new_test', 'get_test_params', 'get_test_history';
+  'create_new_batch_job', 'create_new_test', 'get_test_params', 'get_test_history', 'add_batch_job';
 
 sub user_exists {
     my ( $self, $user ) = @_;
@@ -20,14 +20,14 @@ sub user_exists {
 }
 
 sub add_api_user {
-    my ( $self, $params ) = @_;
+    my ( $self, $username, $api_key ) = @_;
 
     die "username or api_key not provided to the method add_api_user\n"
-      unless ( $params->{username} && $params->{api_key} );
+      unless ( $username && $api_key );
 
-    die "User already exists\n" if ( $self->user_exists( $params->{username} ) );
+    die "User already exists\n" if ( $self->user_exists( $username ) );
 
-    my $result = $self->add_api_user_to_db( $params );
+    my $result = $self->add_api_user_to_db( $username, $api_key );
 
     die "add_api_user_to_db not successfull" unless ( $result );
 
@@ -57,8 +57,16 @@ sub get_test_request {
 
     my $result_id;
     my $dbh = $self->dbh;
-    my ( $id, $hash_id ) = $dbh->selectrow_array(
-        q[ SELECT id, hash_id FROM test_results WHERE progress=0 ORDER BY priority ASC, id ASC LIMIT 1 ] );
+    
+    
+    my ( $id, $hash_id );
+    my $lock_on_queue = Zonemaster::WebBackend::Config->lock_on_queue();
+	if ( defined $lock_on_queue ) {
+		( $id, $hash_id ) = $dbh->selectrow_array( qq[ SELECT id, hash_id FROM test_results WHERE progress=0 AND queue=? ORDER BY priority DESC, id ASC LIMIT 1 ], undef, $lock_on_queue );
+	}
+	else {
+		( $id, $hash_id ) = $dbh->selectrow_array( q[ SELECT id, hash_id FROM test_results WHERE progress=0 ORDER BY priority DESC, id ASC LIMIT 1 ] );
+	}
         
     if ($id) {
 		$dbh->do( q[UPDATE test_results SET progress=1 WHERE id=?], undef, $id );
@@ -74,6 +82,35 @@ sub get_test_request {
 	return $result_id;
 }
 
+# Standatd SQL, can be here
+sub get_batch_job_result {
+	my ( $self, $batch_id ) = @_;
+
+	my $dbh = $self->dbh;
+
+	my %result;
+	$result{nb_running} = 0;
+	$result{nb_finished} = 0;
+
+	my $query = "
+		SELECT hash_id, progress
+		FROM test_results 
+		WHERE batch_id=?";
+		
+	my $sth1 = $dbh->prepare( $query );
+	$sth1->execute( $batch_id );
+	while ( my $h = $sth1->fetchrow_hashref ) {
+		if ( $h->{progress} eq '100' ) {
+			$result{nb_finished}++;
+			push(@{$result{finished_test_ids}}, $h->{hash_id});
+		}
+		else {
+			$result{nb_running}++;
+		}
+	}
+	
+	return \%result;
+}
 
 no Moose::Role;
 
diff --git a/lib/Zonemaster/WebBackend/DB/MySQL.pm b/lib/Zonemaster/WebBackend/DB/MySQL.pm
index c8b4706..d9d7960 100644
--- a/lib/Zonemaster/WebBackend/DB/MySQL.pm
+++ b/lib/Zonemaster/WebBackend/DB/MySQL.pm
@@ -1,10 +1,11 @@
 package Zonemaster::WebBackend::DB::MySQL;
 
-our $VERSION = '1.0.5';
+our $VERSION = '1.1.0';
 
 use Moose;
 use 5.14.2;
 
+use Encode;
 use DBI qw(:utils);
 use JSON::PP;
 use Digest::MD5 qw(md5_hex);
@@ -47,14 +48,14 @@ sub user_exists_in_db {
 }
 
 sub add_api_user_to_db {
-    my ( $self, $user_info ) = @_;
+    my ( $self, $user_name, $api_key  ) = @_;
 
     my $nb_inserted = $self->dbh->do(
         "INSERT INTO users (user_info, username, api_key) VALUES (?,?,?)",
         undef,
-        encode_json( $user_info ),
-        $user_info->{username},
-        $user_info->{api_key},
+        'NULL',
+        $user_name,
+        $api_key,
     );
 
     return $nb_inserted;
@@ -95,10 +96,16 @@ sub create_new_batch_job {
 }
 
 sub create_new_test {
-    my ( $self, $domain, $test_params, $minutes_between_tests_with_same_params, $priority, $batch_id ) = @_;
+    my ( $self, $domain, $test_params, $minutes_between_tests_with_same_params, $batch_id ) = @_;
     my $result;
     my $dbh = $self->dbh;
 
+    my $priority = 10;
+    $priority = $test_params->{priority} if (defined $test_params->{priority});
+    
+    my $queue = 0;
+    $queue = $test_params->{queue} if (defined $test_params->{queue});
+
     $test_params->{domain} = $domain;
     my $js                             = JSON->new->canonical;
     my $encoded_params                 = $js->encode( $test_params );
@@ -126,11 +133,12 @@ SELECT id, hash_id FROM test_results WHERE params_deterministic_hash = ? AND (TO
         else {
             $dbh->do(
                 q[
-            INSERT INTO test_results (batch_id, priority, params_deterministic_hash, params, domain, test_start_time, undelegated) VALUES (?,?,?,?,?, NOW(),?)
+            INSERT INTO test_results (batch_id, priority, queue, params_deterministic_hash, params, domain, test_start_time, undelegated) VALUES (?, ?,?,?,?,?, NOW(),?)
         ],
                 undef,
                 $batch_id,
                 $priority,
+                $queue,
                 $test_params_deterministic_hash,
                 $encoded_params,
                 $test_params->{domain},
@@ -159,8 +167,14 @@ sub test_progress {
 	my $id_field = $self->_get_allowed_id_field_name($test_id);
 
 	my $dbh = $self->dbh;
-	$dbh->do( "UPDATE test_results SET progress=?,test_end_time=NOW() WHERE $id_field=?", undef, $progress, $test_id )
-      if ( $progress );
+    if ( $progress ) {
+		if ($progress == 1) {
+			$dbh->do( "UPDATE test_results SET progress=?, test_start_time=NOW() WHERE $id_field=?", undef, $progress, $test_id );
+		}
+		else {
+			$dbh->do( "UPDATE test_results SET progress=? WHERE $id_field=?", undef, $progress, $test_id );
+		}
+	}
 
     my ( $result ) = $self->dbh->selectrow_array( "SELECT progress FROM test_results WHERE $id_field=?", undef, $test_id );
 
@@ -194,7 +208,7 @@ sub test_results {
     }
 
     my $result;
-    my ( $hrefs ) = $self->dbh->selectall_hashref( "SELECT * FROM test_results WHERE $id_field=?", $id_field, undef, $test_id );
+    my ( $hrefs ) = $self->dbh->selectall_hashref( "SELECT id, hash_id, CONVERT_TZ(`creation_time`, \@\@session.time_zone, '+00:00') AS creation_time, params, results FROM test_results WHERE $id_field=?", $id_field, undef, $test_id );
     $result            = $hrefs->{$test_id};
     $result->{params}  = decode_json( $result->{params} );
     $result->{results} = decode_json( $result->{results} );
@@ -210,7 +224,19 @@ sub get_test_history {
     my $use_hash_id_from_id = Zonemaster::WebBackend::Config->force_hash_id_use_in_API_starting_from_id();
     
     my $sth = $self->dbh->prepare(
-q[SELECT id, hash_id, creation_time, params, results FROM test_results WHERE domain = ? AND undelegated = ? ORDER BY id DESC LIMIT ? OFFSET ?]
+			q[SELECT 
+				id, 
+				hash_id, 
+				CONVERT_TZ(`creation_time`, @@session.time_zone, '+00:00') AS creation_time, 
+				params, 
+				results 
+			FROM 
+				test_results 
+			WHERE 
+				domain = ? 
+				AND undelegated = ? 
+			ORDER BY id DESC 
+			LIMIT ? OFFSET ?]
     );
     $sth->execute( $p->{frontend_params}{domain}, ($p->{frontend_params}{nameservers})?1:0, $p->{limit}, $p->{offset} );
     while ( my $h = $sth->fetchrow_hashref ) {
@@ -240,6 +266,58 @@ q[SELECT id, hash_id, creation_time, params, results FROM test_results WHERE dom
     return \@results;
 }
 
+sub add_batch_job {
+    my ( $self, $params ) = @_;
+    my $batch_id;
+
+	my $dbh = $self->dbh;
+	my $js = JSON->new;
+	$js->canonical( 1 );
+    		
+    if ( $self->user_authorized( $params->{username}, $params->{api_key} ) ) {
+        $params->{test_params}->{client_id}      = 'Zonemaster Batch Scheduler';
+        $params->{test_params}->{client_version} = '1.0';
+        $params->{test_params}->{priority} = 5 unless (defined $params->{test_params}->{priority});
+
+        $batch_id = $self->create_new_batch_job( $params->{username} );
+
+        my $minutes_between_tests_with_same_params = 5;
+		my $test_params = $params->{test_params};
+		
+		my $priority = 10;
+		$priority = $test_params->{priority} if (defined $test_params->{priority});
+		
+		my $queue = 0;
+		$queue = $test_params->{queue} if (defined $test_params->{queue});
+		
+		$dbh->{AutoCommit} = 0;
+		eval {$dbh->do( "DROP INDEX test_results__hash_id ON test_results" );};
+		eval {$dbh->do( "DROP INDEX test_results__params_deterministic_hash ON test_results" );};
+		eval {$dbh->do( "DROP INDEX test_results__batch_id_progress ON test_results" );};
+		
+		my $sth = $dbh->prepare( 'INSERT INTO test_results (domain, batch_id, priority, queue, params_deterministic_hash, params) VALUES (?, ?, ?, ?, ?, ?) ' );
+        foreach my $domain ( @{$params->{domains}} ) {
+			$test_params->{domain} = $domain;
+			my $encoded_params                 = $js->encode( $test_params );
+			my $test_params_deterministic_hash = md5_hex( encode_utf8( $encoded_params ) );
+
+			$sth->execute( $test_params->{domain}, $batch_id, $priority, $queue, $test_params_deterministic_hash, $encoded_params );
+        }
+		$dbh->do( "CREATE INDEX test_results__hash_id ON test_results (hash_id, creation_time)" );
+		$dbh->do( "CREATE INDEX test_results__params_deterministic_hash ON test_results (params_deterministic_hash)" );
+		$dbh->do( "CREATE INDEX test_results__batch_id_progress ON test_results (batch_id, progress)" );
+       
+        $dbh->commit();
+        $dbh->{AutoCommit} = 1;
+    }
+    else {
+        die "User $params->{username} not authorized to use batch mode\n";
+    }
+
+    return $batch_id;
+}
+
+
 no Moose;
 __PACKAGE__->meta()->make_immutable();
 
diff --git a/lib/Zonemaster/WebBackend/DB/PostgreSQL.pm b/lib/Zonemaster/WebBackend/DB/PostgreSQL.pm
index 1260425..c39fd90 100644
--- a/lib/Zonemaster/WebBackend/DB/PostgreSQL.pm
+++ b/lib/Zonemaster/WebBackend/DB/PostgreSQL.pm
@@ -1,6 +1,6 @@
 package Zonemaster::WebBackend::DB::PostgreSQL;
 
-our $VERSION = '1.0.5';
+our $VERSION = '1.1.0';
 
 use Moose;
 use 5.14.2;
@@ -50,10 +50,10 @@ sub user_exists_in_db {
 }
 
 sub add_api_user_to_db {
-    my ( $self, $user_info ) = @_;
+    my ( $self, $user_name, $api_key ) = @_;
 
     my $dbh = $self->dbh;
-    my $nb_inserted = $dbh->do( "INSERT INTO users (user_info) VALUES (?)", undef, encode_json( $user_info ) );
+    my $nb_inserted = $dbh->do( "INSERT INTO users (user_info) VALUES (?)", undef, encode_json( { username => $user_name, api_key => $api_key } ) );
 
     return $nb_inserted;
 }
@@ -74,7 +74,14 @@ sub test_progress {
 
 	my $id_field = $self->_get_allowed_id_field_name($test_id);
     my $dbh = $self->dbh;
-    $dbh->do( "UPDATE test_results SET progress=$progress WHERE $id_field=?", undef, $test_id ) if ( $progress );
+    if ( $progress ) {
+		if ($progress == 1) {
+			$dbh->do( "UPDATE test_results SET progress=?, test_start_time=NOW() WHERE $id_field=?", undef, $progress, $test_id );
+		}
+		else {
+			$dbh->do( "UPDATE test_results SET progress=? WHERE $id_field=?", undef, $progress, $test_id );
+		}
+	}
 	
     my ( $result ) = $dbh->selectrow_array( "SELECT progress FROM test_results WHERE $id_field=?", undef, $test_id );
 
@@ -85,7 +92,7 @@ sub create_new_batch_job {
     my ( $self, $username ) = @_;
 
     my $dbh = $self->dbh;
-    my ( $batch_id, $creaton_time ) = $dbh->selectrow_array( "
+    my ( $batch_id, $creation_time ) = $dbh->selectrow_array( "
 			SELECT 
 				batch_id, 
 				batch_jobs.creation_time AS batch_creation_time 
@@ -98,7 +105,7 @@ sub create_new_batch_job {
 			LIMIT 1
 			", undef, $username );
 
-    die "You can't create a new batch job, job:[$batch_id] started on:[$creaton_time] still running " if ( $batch_id );
+    die "You can't create a new batch job, job:[$batch_id] started on:[$creation_time] still running " if ( $batch_id );
 
     my ( $new_batch_id ) =
       $dbh->selectrow_array( "INSERT INTO batch_jobs (username) VALUES (?) RETURNING id", undef, $username );
@@ -107,10 +114,16 @@ sub create_new_batch_job {
 }
 
 sub create_new_test {
-    my ( $self, $domain, $test_params, $minutes_between_tests_with_same_params, $priority, $batch_id ) = @_;
+    my ( $self, $domain, $test_params, $minutes_between_tests_with_same_params, $batch_id ) = @_;
     my $result;
     my $dbh = $self->dbh;
 
+    my $priority = 10;
+    $priority = $test_params->{priority} if (defined $test_params->{priority});
+    
+    my $queue = 0;
+    $queue = $test_params->{queue} if (defined $test_params->{queue});
+    
     $test_params->{domain} = $domain;
     my $js = JSON->new;
     $js->canonical( 1 );
@@ -118,9 +131,10 @@ sub create_new_test {
     my $test_params_deterministic_hash = md5_hex( encode_utf8( $encoded_params ) );
 
     my $query =
-        "INSERT INTO test_results (batch_id, priority, params_deterministic_hash, params) SELECT "
+        "INSERT INTO test_results (batch_id, priority, queue, params_deterministic_hash, params) SELECT "
       . $dbh->quote( $batch_id ) . ", "
-      . $dbh->quote( 5 ) . ", "
+      . $dbh->quote( $priority ) . ", "
+      . $dbh->quote( $queue ) . ", "
       . $dbh->quote( $test_params_deterministic_hash ) . ", "
       . $dbh->quote( $encoded_params )
       . " WHERE NOT EXISTS (SELECT * FROM test_results WHERE params_deterministic_hash='$test_params_deterministic_hash' AND creation_time > NOW()-'$minutes_between_tests_with_same_params minutes'::interval)";
@@ -165,7 +179,7 @@ sub test_results {
 
     my $result;
     eval {
-        my ( $hrefs ) = $dbh->selectall_hashref( "SELECT * FROM test_results WHERE $id_field=?", $id_field, undef, $test_id );
+        my ( $hrefs ) = $dbh->selectall_hashref( "SELECT id, hash_id, creation_time at time zone current_setting('TIMEZONE') at time zone 'UTC' as creation_time, params, results FROM test_results WHERE $id_field=?", $id_field, undef, $test_id );
         $result            = $hrefs->{$test_id};
         $result->{params}  = decode_json( encode_utf8( $result->{params} ) );
         $result->{results} = decode_json( encode_utf8( $result->{results} ) );
@@ -197,7 +211,7 @@ sub get_test_history {
 			(SELECT count(*) FROM (SELECT json_array_elements(results) AS result) AS t1 WHERE result->>'level'='WARNING') AS nb_warning,
 			id,
 			hash_id,
-			creation_time, 
+			creation_time at time zone current_setting('TIMEZONE') at time zone 'UTC' as creation_time, 
 			params->>'advanced_options' AS advanced_options 
 		FROM test_results 
 		WHERE params->>'domain'=" . $dbh->quote( $p->{frontend_params}->{domain} ) . " $undelegated 
@@ -231,6 +245,60 @@ sub get_test_history {
 	return \@results;
 }
 
+sub add_batch_job {
+    my ( $self, $params ) = @_;
+    my $batch_id;
+
+	my $dbh = $self->dbh;
+	my $js = JSON->new;
+	$js->canonical( 1 );
+    		
+    if ( $self->user_authorized( $params->{username}, $params->{api_key} ) ) {
+        $params->{test_params}->{client_id}      = 'Zonemaster Batch Scheduler';
+        $params->{test_params}->{client_version} = '1.0';
+        $params->{test_params}->{priority} = 5 unless (defined $params->{test_params}->{priority});
+
+        $batch_id = $self->create_new_batch_job( $params->{username} );
+
+        my $minutes_between_tests_with_same_params = 5;
+		my $test_params = $params->{test_params};
+		
+		my $priority = 10;
+		$priority = $test_params->{priority} if (defined $test_params->{priority});
+		
+		my $queue = 0;
+		$queue = $test_params->{queue} if (defined $test_params->{queue});
+		
+		$dbh->begin_work();
+		$dbh->do( "ALTER TABLE test_results DROP CONSTRAINT IF EXISTS test_results_pkey" );
+		$dbh->do( "DROP INDEX IF EXISTS test_results__hash_id" );
+		$dbh->do( "DROP INDEX IF EXISTS test_results__params_deterministic_hash" );
+		$dbh->do( "DROP INDEX IF EXISTS test_results__batch_id_progress" );
+		
+		$dbh->do( "COPY test_results(batch_id, priority, queue, params_deterministic_hash, params) FROM STDIN" );
+        foreach my $domain ( @{$params->{domains}} ) {
+			$test_params->{domain} = $domain;
+			my $encoded_params                 = $js->encode( $test_params );
+			my $test_params_deterministic_hash = md5_hex( encode_utf8( $encoded_params ) );
+
+			$dbh->pg_putcopydata("$batch_id\t$priority\t$queue\t$test_params_deterministic_hash\t$encoded_params\n");
+        }
+        $dbh->pg_putcopyend();
+		$dbh->do( "ALTER TABLE test_results ADD PRIMARY KEY (id)" );
+		$dbh->do( "CREATE INDEX test_results__hash_id ON test_results (hash_id, creation_time)" );
+		$dbh->do( "CREATE INDEX test_results__params_deterministic_hash ON test_results (params_deterministic_hash)" );
+        $dbh->do( "CREATE INDEX test_results__batch_id_progress ON test_results (batch_id, progress)" );
+        
+        $dbh->commit();
+    }
+    else {
+        die "User $params->{username} not authorized to use batch mode\n";
+    }
+
+    return $batch_id;
+}
+
+
 no Moose;
 __PACKAGE__->meta()->make_immutable();
 
diff --git a/lib/Zonemaster/WebBackend/DB/SQLite.pm b/lib/Zonemaster/WebBackend/DB/SQLite.pm
index d699158..2cec34c 100644
--- a/lib/Zonemaster/WebBackend/DB/SQLite.pm
+++ b/lib/Zonemaster/WebBackend/DB/SQLite.pm
@@ -1,6 +1,6 @@
 package Zonemaster::WebBackend::DB::SQLite;
 
-our $VERSION = '1.0.5';
+our $VERSION = '1.1.0';
 
 use Moose;
 use 5.14.2;
@@ -44,6 +44,7 @@ sub create_db {
 					test_start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
 					test_end_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
 					priority integer DEFAULT 10,
+					queue integer DEFAULT 0,
 					progress integer DEFAULT 0,
 					params_deterministic_hash character varying(32),
 					params text NOT NULL,
@@ -95,10 +96,10 @@ sub user_exists_in_db {
 }
 
 sub add_api_user_to_db {
-    my ( $self, $user_info ) = @_;
+    my ( $self, $user_name, $api_key ) = @_;
 
-    my $nb_inserted =
-      $self->dbh->do( "INSERT INTO users (user_info) VALUES(" . $self->dbh->quote( encode_json( $user_info ) ) . ")" );
+    my $dbh = $self->dbh;
+    my $nb_inserted = $dbh->do( "INSERT INTO users (user_info) VALUES (?)", undef, encode_json( { username => $user_name, api_key => $api_key } ) );
 
     return $nb_inserted;
 }
@@ -144,9 +145,15 @@ sub create_new_batch_job {
 }
 
 sub create_new_test {
-    my ( $self, $domain, $test_params, $minutes_between_tests_with_same_params, $priority, $batch_id ) = @_;
+    my ( $self, $domain, $test_params, $minutes_between_tests_with_same_params, $batch_id ) = @_;
     my $result;
 
+    my $priority = 10;
+    $priority = $test_params->{priority} if (defined $test_params->{priority});
+    
+    my $queue = 0;
+    $queue = $test_params->{queue} if (defined $test_params->{queue});
+
     $test_params->{domain} = $domain;
     my $js = JSON->new;
     $js->canonical( 1 );
@@ -154,9 +161,10 @@ sub create_new_test {
     my $test_params_deterministic_hash = md5_hex( $encoded_params );
 
     my $query =
-        "INSERT INTO test_results (batch_id, priority, params_deterministic_hash, params) SELECT "
+        "INSERT INTO test_results (batch_id, priority, queue, params_deterministic_hash, params) SELECT "
       . $self->dbh->quote( $batch_id ) . ", "
-      . $self->dbh->quote( 5 ) . ", "
+      . $self->dbh->quote( $priority ) . ", "
+      . $self->dbh->quote( $queue ) . ", "
       . $self->dbh->quote( $test_params_deterministic_hash ) . ", "
       . $self->dbh->quote( $encoded_params )
       . " WHERE NOT EXISTS (SELECT * FROM test_results WHERE params_deterministic_hash='$test_params_deterministic_hash' AND creation_time > datetime('now', '-$minutes_between_tests_with_same_params minute'))";
@@ -240,6 +248,10 @@ sub get_test_history {
     return \@results;
 }
 
+sub add_batch_job {
+    my ( $self, $params ) = @_;
+}
+
 no Moose;
 __PACKAGE__->meta()->make_immutable();
 
diff --git a/lib/Zonemaster/WebBackend/Engine.pm b/lib/Zonemaster/WebBackend/Engine.pm
index 5d9add2..5d5a8bd 100644
--- a/lib/Zonemaster/WebBackend/Engine.pm
+++ b/lib/Zonemaster/WebBackend/Engine.pm
@@ -1,6 +1,6 @@
 package Zonemaster::WebBackend::Engine;
 
-our $VERSION = '1.0.5';
+our $VERSION = '1.1.0';
 
 use strict;
 use warnings;
@@ -143,16 +143,6 @@ sub _check_domain {
         return ( $dn, { status => 'nok', message => encode_entities( "$type name or label outside allowed length" ) } );
     }
 
-    @res = Zonemaster::Test::Syntax->syntax01($dn);
-    if (not grep {$_->tag eq 'ONLY_ALLOWED_CHARS'} @res) {
-        return ( $dn, { status => 'nok', message => encode_entities( "$type name contains non-allowed character(s)" ) } );
-    }
-
-    @res = Zonemaster::Test::Syntax->syntax02($dn);
-    if (not grep {$_->tag eq 'NO_ENDING_HYPHENS'} @res) {
-        return ( $dn, { status => 'nok', message => encode_entities( "$type label must not start or end with a hyphen" ) } );
-    }
-
     return ( $dn, { status => 'ok', message => 'Syntax ok' } );
 }
 
@@ -161,7 +151,7 @@ sub validate_syntax {
 
     my @allowed_params_keys = (
         'domain',   'ipv4',      'ipv6', 'ds_info', 'nameservers', 'profile',
-        'advanced', 'client_id', 'client_version', 'user_ip', 'user_location_info'
+        'advanced', 'client_id', 'client_version', 'user_ip', 'user_location_info', 'config', 'priority', 'queue'
     );
 
     foreach my $k ( keys %$syntax_input ) {
@@ -185,35 +175,32 @@ sub validate_syntax {
         }
     }
 
-    return { status => 'nok', message => encode_entities( "At least one transport protocol required (IPv4 or IPv6)" ) }
-      unless ( $syntax_input->{ipv4} || $syntax_input->{ipv6} );
-
     if ( defined $syntax_input->{advanced} ) {
         return { status => 'nok', message => encode_entities( "Invalid 'advanced' option format" ) }
-          unless ( $syntax_input->{advanced} ne JSON::false || $syntax_input->{advanced} ne JSON::true );
+          unless ( $syntax_input->{advanced} eq JSON::false || $syntax_input->{advanced} eq JSON::true );
     }
 
     if ( defined $syntax_input->{ipv4} ) {
         return { status => 'nok', message => encode_entities( "Invalid IPv4 transport option format" ) }
-          unless ( $syntax_input->{ipv4} ne JSON::false
-            || $syntax_input->{ipv4} ne JSON::true
-            || $syntax_input->{ipv4} ne '1'
-            || $syntax_input->{ipv4} ne '0' );
+          unless ( $syntax_input->{ipv4} eq JSON::false
+            || $syntax_input->{ipv4} eq JSON::true
+            || $syntax_input->{ipv4} eq '1'
+            || $syntax_input->{ipv4} eq '0' );
     }
 
     if ( defined $syntax_input->{ipv6} ) {
         return { status => 'nok', message => encode_entities( "Invalid IPv6 transport option format" ) }
-          unless ( $syntax_input->{ipv6} ne JSON::false
-            || $syntax_input->{ipv6} ne JSON::true
-            || $syntax_input->{ipv6} ne '1'
-            || $syntax_input->{ipv6} ne '0' );
+          unless ( $syntax_input->{ipv6} eq JSON::false
+            || $syntax_input->{ipv6} eq JSON::true
+            || $syntax_input->{ipv6} eq '1'
+            || $syntax_input->{ipv6} eq '0' );
     }
 
     if ( defined $syntax_input->{profile} ) {
         return { status => 'nok', message => encode_entities( "Invalid profile option format" ) }
-          unless ( $syntax_input->{profile} ne 'default_profile'
-            || $syntax_input->{profile} ne 'test_profile_1'
-            || $syntax_input->{profile} ne 'test_profile_2' );
+          unless ( $syntax_input->{profile} eq 'default_profile'
+            || $syntax_input->{profile} eq 'test_profile_1'
+            || $syntax_input->{profile} eq 'test_profile_2' );
     }
 
     my ( $dn, $dn_syntax ) = $self->_check_domain( $syntax_input->{domain}, 'Domain name' );
@@ -228,7 +215,7 @@ sub validate_syntax {
 
         foreach my $ns_ip ( @{ $syntax_input->{nameservers} } ) {
             return { status => 'nok', message => encode_entities( "Invalid IP address: [$ns_ip->{ip}]" ) }
-              unless ( ip_is_ipv4( $ns_ip->{ip} ) || ip_is_ipv6( $ns_ip->{ip} ) );
+              unless ( !$ns_ip->{ip} || ip_is_ipv4( $ns_ip->{ip} ) || ip_is_ipv6( $ns_ip->{ip} ) );
         }
 
         foreach my $ds_digest ( @{ $syntax_input->{ds_info} } ) {
@@ -291,9 +278,14 @@ sub start_domain_test {
 
     die "No domain in parameters\n" unless ( $params->{domain} );
     
+    if ($params->{config}) {
+		$params->{config} =~ s/[^\w_]//isg;
+		die "Unknown test configuration: [$params->{config}]\n" unless ( Zonemaster::WebBackend::Config->GetCustomConfigParameter('ZONEMASTER', $params->{config}) );
+	}
+    
     $self->add_user_ip_geolocation($params);
 
-    $result = $self->{db}->create_new_test( $params->{domain}, $params, 10, 10 );
+    $result = $self->{db}->create_new_test( $params->{domain}, $params, 10 );
 
     return $result;
 }
@@ -355,13 +347,13 @@ sub get_test_results {
             if ( $res->{message} =~ /policy\.json/ ) {
                 my ( $policy ) = ( $res->{message} =~ /\s(\/.*)$/ );
                 my $policy_description = 'DEFAULT POLICY';
-                $policy_description = 'SOME OTHER POLICY' if ( $policy =~ /some\/other\/policy\path/ );
+                $policy_description = 'SOME OTHER POLICY' if ( $policy =~ /some\/other\/policy\/path/ );
                 $res->{message} =~ s/$policy/$policy_description/;
             }
             elsif ( $res->{message} =~ /config\.json/ ) {
                 my ( $config ) = ( $res->{message} =~ /\s(\/.*)$/ );
                 my $config_description = 'DEFAULT CONFIGURATION';
-                $config_description = 'SOME OTHER CONFIGURATION' if ( $config =~ /some\/other\/configuration\path/ );
+                $config_description = 'SOME OTHER CONFIGURATION' if ( $config =~ /some\/other\/configuration\/path/ );
                 $res->{message} =~ s/$config/$config_description/;
             }
         }
@@ -384,40 +376,43 @@ sub get_test_history {
 }
 
 sub add_api_user {
-    my ( $self, $params, $procedure, $remote_ip ) = @_;
-    my $result;
+    my ( $self, $p, undef, $remote_ip ) = @_;
+    my $result = 0;
 
     my $allow = 0;
-    if ( defined $procedure && defined $remote_ip ) {
-        $allow = 1 if ( $remote_ip eq '::1' );
+    if ( defined $remote_ip ) {
+        $allow = 1 if ( $remote_ip eq '::1' || $remote_ip eq '127.0.0.1' );
     }
     else {
         $allow = 1;
     }
 
     if ( $allow ) {
-        $result = $self->{db}->add_api_user( $params );
+		$result = 1 if ( $self->{db}->add_api_user( $p->{username}, $p->{api_key} ) eq '1' );
     }
+    
+    return $result;
 }
 
+=coment
 sub add_batch_job {
     my ( $self, $params ) = @_;
     my $batch_id;
 
     if ( $self->{db}->user_authorized( $params->{username}, $params->{api_key} ) ) {
-        $params->{batch_params}->{client_id}      = 'Zonemaster Batch Scheduler';
-        $params->{batch_params}->{client_version} = '1.0';
-
-        my $domains = $params->{batch_params}->{domains};
-        delete( $params->{batch_params}->{domains} );
+        $params->{test_params}->{client_id}      = 'Zonemaster Batch Scheduler';
+        $params->{test_params}->{client_version} = '1.0';
+        $params->{test_params}->{priority} = 5 unless (defined $params->{test_params}->{priority});
 
         $batch_id = $self->{db}->create_new_batch_job( $params->{username} );
 
         my $minutes_between_tests_with_same_params = 5;
-        foreach my $domain ( @{$domains} ) {
+#        $self->{db}->dbhandle->begin_work();
+        foreach my $domain ( @{$params->{domains}} ) {
             $self->{db}
-              ->create_new_test( $domain, $params->{batch_params}, $minutes_between_tests_with_same_params, $batch_id );
+              ->create_new_test( $domain, $params->{test_params}, 5, $batch_id );
         }
+#        $self->{db}->dbhandle->commit();
     }
     else {
         die "User $params->{username} not authorized to use batch mode\n";
@@ -425,5 +420,20 @@ sub add_batch_job {
 
     return $batch_id;
 }
+=cut
+
+sub add_batch_job {
+    my ( $self, $params ) = @_;
+
+    my $results = $self->{db}->add_batch_job( $params );
 
+    return $results;
+}
+
+
+sub get_batch_job_result {
+    my ( $self, $batch_id ) = @_;
+
+    return $self->{db}->get_batch_job_result($batch_id);
+}
 1;
diff --git a/lib/Zonemaster/WebBackend/Runner.pm b/lib/Zonemaster/WebBackend/Runner.pm
index f27efad..3bbf1ef 100644
--- a/lib/Zonemaster/WebBackend/Runner.pm
+++ b/lib/Zonemaster/WebBackend/Runner.pm
@@ -1,5 +1,5 @@
 package Zonemaster::WebBackend::Runner;
-our $VERSION = '1.0.5';
+our $VERSION = '1.1.0';
 
 use strict;
 use warnings;
@@ -58,8 +58,14 @@ sub run {
     }
     $domain = $self->to_idn( $domain );
 
-    Zonemaster->config->get->{net}{ipv4} = ( $params->{ipv4} ) ? ( 1 ) : ( 0 );
-    Zonemaster->config->get->{net}{ipv6} = ( $params->{ipv6} ) ? ( 1 ) : ( 0 );
+    if (defined $params->{ipv4} || defined $params->{ipv4}) {
+		Zonemaster->config->get->{net}{ipv4} = ( $params->{ipv4} ) ? ( 1 ) : ( 0 );
+		Zonemaster->config->get->{net}{ipv6} = ( $params->{ipv6} ) ? ( 1 ) : ( 0 );
+	}
+	else {
+		Zonemaster->config->get->{net}{ipv4} = 1;
+		Zonemaster->config->get->{net}{ipv6} = 1;
+	}
 
     # used for progress indicator
     my ( $previous_module, $previous_method ) = ( '', '' );
@@ -123,6 +129,21 @@ sub run {
 		}
 	}
 
+	if ( $params->{config} ) {
+		my $config_file_path = Zonemaster::WebBackend::Config->GetCustomConfigParameter('ZONEMASTER', $params->{config});
+		if ($config_file_path) {
+			if (-e $config_file_path) {
+				Zonemaster->config->load_config_file( $config_file_path );
+			}
+			else {
+				die "The file specified by the config parameter value: [$params->{config}] doesn't exist";
+			}
+		}
+		else {
+			die "Unknown test configuration: [$params->{config}]\n"
+		}
+	}
+
     # Actually run tests!
     eval { Zonemaster->test_zone( $domain ); };
     if ( $@ ) {
@@ -147,8 +168,16 @@ sub add_fake_delegation {
     my %data;
 
     foreach my $ns_ip_pair ( @$nameservers ) {
-        push( @{ $data{ $self->to_idn( $ns_ip_pair->{ns} ) } }, $ns_ip_pair->{ip} )
-          if ( $ns_ip_pair->{ns} && $ns_ip_pair->{ip} );
+		if ( $ns_ip_pair->{ns} && $ns_ip_pair->{ip} ) {
+			push( @{ $data{ $self->to_idn( $ns_ip_pair->{ns} ) } }, $ns_ip_pair->{ip} );
+		}
+		elsif ($ns_ip_pair->{ns}) {
+			my @ips = Net::LDNS->new->name2addr($ns_ip_pair->{ns});
+			push( @{ $data{ $self->to_idn( $ns_ip_pair->{ns} ) } }, $_) for @ips;
+		}
+		else {
+			die "Invalid ns_ip_pair";
+		}
     }
 
     Zonemaster->add_fake_delegation( $domain => \%data );
diff --git a/lib/Zonemaster/WebBackend/Translator.pm b/lib/Zonemaster/WebBackend/Translator.pm
index c449b6c..1cccfa5 100644
--- a/lib/Zonemaster/WebBackend/Translator.pm
+++ b/lib/Zonemaster/WebBackend/Translator.pm
@@ -1,6 +1,6 @@
 package Zonemaster::WebBackend::Translator;
 
-our $VERSION = '1.0.5';
+our $VERSION = '1.1.0';
 
 use 5.14.2;
 
@@ -18,6 +18,7 @@ extends 'Zonemaster::Translator';
 sub translate_tag {
     my ( $self, $entry, $browser_lang ) = @_;
 
+    my $previous_locale = setlocale( LC_ALL );
     if ( $browser_lang eq 'fr' ) {
         setlocale( LC_ALL, "fr_FR.UTF-8" );
     }
@@ -27,7 +28,6 @@ sub translate_tag {
     else {
         setlocale( LC_ALL, "en_US.UTF-8" );
     }
-
     my $string = $self->data->{ $entry->{module} }{ $entry->{tag} };
 
     if ( not $string ) {
@@ -36,7 +36,7 @@ sub translate_tag {
 
     my $blessed_entry = bless($entry, 'Zonemaster::Logger::Entry');
     my $str = decode_utf8( __x( $string, %{ ($blessed_entry->can('printable_args'))?($blessed_entry->printable_args()):($entry->{args}) } ) );
-    setlocale( LC_ALL, "" );
+    setlocale( LC_ALL, $previous_locale );
 
     return $str;
 }
diff --git a/script/create_db_mysql.pl b/script/create_db_mysql.pl
index 3862f87..10e989d 100644
--- a/script/create_db_mysql.pl
+++ b/script/create_db_mysql.pl
@@ -35,6 +35,7 @@ sub create_db {
 			test_start_time TIMESTAMP,
 			test_end_time TIMESTAMP,
 			priority integer DEFAULT 10,
+			queue integer DEFAULT 0,
 			progress integer DEFAULT 0,
 			params_deterministic_hash character varying(32),
 			params blob NOT NULL,
@@ -61,6 +62,14 @@ sub create_db {
 		'CREATE INDEX test_results__hash_id ON test_results (hash_id)'
     );
     
+    $dbh->do(
+		'CREATE INDEX test_results__params_deterministic_hash ON test_results (params_deterministic_hash)'
+    );
+
+    $dbh->do(
+		'CREATE INDEX test_results__batch_id_progress ON test_results (batch_id, progress)'
+    );
+    
     ####################################################################
     # BATCH JOBS
     ####################################################################
diff --git a/script/create_db_postgresql_9.3.pl b/script/create_db_postgresql_9.3.pl
index bf16c02..69d69d0 100644
--- a/script/create_db_postgresql_9.3.pl
+++ b/script/create_db_postgresql_9.3.pl
@@ -46,6 +46,7 @@ sub create_db {
                         test_start_time timestamp without time zone DEFAULT NULL,
                         test_end_time timestamp without time zone DEFAULT NULL,
                         priority integer DEFAULT 10,
+                        queue integer DEFAULT 0,
                         progress integer DEFAULT 0,
                         params_deterministic_hash character varying(32),
                         params json NOT NULL,
@@ -58,6 +59,14 @@ sub create_db {
 		'CREATE INDEX test_results__hash_id ON test_results (hash_id)'
     );
     
+    $dbh->do(
+		'CREATE INDEX test_results__params_deterministic_hash ON test_results (params_deterministic_hash)'
+    );
+
+    $dbh->do(
+		'CREATE INDEX test_results__batch_id_progress ON test_results (batch_id, progress)'
+    );
+    
     $dbh->do( "ALTER TABLE test_results OWNER TO $db_user" );
 
     ####################################################################
diff --git a/script/crontab_job_runner/execute_tests.pl b/script/crontab_job_runner/execute_tests.pl
index b5c07f3..fab1a0f 100644
--- a/script/crontab_job_runner/execute_tests.pl
+++ b/script/crontab_job_runner/execute_tests.pl
@@ -44,8 +44,8 @@ my $LOG_DIR = Zonemaster::WebBackend::Config->LogDir();
 my $perl_command = Zonemaster::WebBackend::Config->PerlIntereter();
 my $polling_interval = Zonemaster::WebBackend::Config->PollingInterval();
 my $zonemaster_timeout_interval = Zonemaster::WebBackend::Config->MaxZonemasterExecutionTime();
-my $frontend_slots = Zonemaster::WebBackend::Config->NumberOfProfessesForFrontendTesting();
-my $batch_slots = Zonemaster::WebBackend::Config->NumberOfProfessesForBatchTesting();
+my $frontend_slots = Zonemaster::WebBackend::Config->NumberOfProcessesForFrontendTesting();
+my $batch_slots = Zonemaster::WebBackend::Config->NumberOfProcessesForBatchTesting();
 
 my $connection_string = Zonemaster::WebBackend::Config->DB_connection_string();
 my $dbh = DBI->connect($connection_string, Zonemaster::WebBackend::Config->DB_user(), Zonemaster::WebBackend::Config->DB_password(), {RaiseError => 1, AutoCommit => 1});
diff --git a/script/zm_wb_daemon b/script/zm_wb_daemon
index 1211f9c..99a703f 100755
--- a/script/zm_wb_daemon
+++ b/script/zm_wb_daemon
@@ -47,8 +47,8 @@ $pidfile //= '/tmp/zm_wc_daemon.pid';
 
 # Yes, the method names are spelled like that.
 my $maximum_processes =
-  Zonemaster::WebBackend::Config->NumberOfProfessesForFrontendTesting() +
-  Zonemaster::WebBackend::Config->NumberOfProfessesForBatchTesting();
+  Zonemaster::WebBackend::Config->NumberOfProcessesForFrontendTesting() +
+  Zonemaster::WebBackend::Config->NumberOfProcessesForBatchTesting();
 
 my $delay   = Zonemaster::WebBackend::Config->PollingInterval();
 my $timeout = Zonemaster::WebBackend::Config->MaxZonemasterExecutionTime();
diff --git a/script/zonemaster_webbackend.psgi b/script/zonemaster_webbackend.psgi
index a690f38..5738f58 100644
--- a/script/zonemaster_webbackend.psgi
+++ b/script/zonemaster_webbackend.psgi
@@ -1,7 +1,7 @@
 use strict;
 use warnings;
 
-our $VERSION = '1.0.5';
+our $VERSION = '1.1.0';
 
 use 5.14.2;
 
@@ -75,11 +75,15 @@ my $router = router {
 		handler => "+Zonemaster::WebBackend::Engine",
 		action => "add_api_user"
 	};
-	
-############################################
-	connect "api1" => {
+
+	connect "add_batch_job" => {
+		handler => "+Zonemaster::WebBackend::Engine",
+		action => "add_batch_job"
+	};
+
+	connect "get_batch_job_result" => {
 		handler => "+Zonemaster::WebBackend::Engine",
-		action => "api1"
+		action => "get_batch_job_result"
 	};
 };
 
diff --git a/share/backend_config.ini b/share/backend_config.ini
index 19c82fe..1e39b84 100755
--- a/share/backend_config.ini
+++ b/share/backend_config.ini
@@ -17,13 +17,15 @@ interpreter=perl
 
 [ZONEMASTER]
 max_zonemaster_execution_time=300
-number_of_professes_for_frontend_testing=20
-number_of_professes_for_batch_testing=20
+number_of_processes_for_frontend_testing=20
+number_of_processes_for_batch_testing=20
 #seconds
+config_logfilter_1=/full/path/to/a/config_file.json
+# see zonemaster-engine documentation
 
 [GEOLOCATION]
 # Requires the Geo::IP Perl module to be installed
 #maxmind_isp_db_file=
 
 # Requires the GeoIP2::Database::Reader Perl module to be installed
-#maxmind_city_db_file=
\ No newline at end of file
+#maxmind_city_db_file=
diff --git a/docs/cleanup-mysql.sql b/share/cleanup-mysql.sql
similarity index 100%
rename from docs/cleanup-mysql.sql
rename to share/cleanup-mysql.sql
diff --git a/docs/cleanup-postgres.sql b/share/cleanup-postgres.sql
similarity index 100%
rename from docs/cleanup-postgres.sql
rename to share/cleanup-postgres.sql
diff --git a/docs/initial-mysql.sql b/share/initial-mysql.sql
similarity index 79%
rename from docs/initial-mysql.sql
rename to share/initial-mysql.sql
index bcf530e..3d9eb26 100644
--- a/docs/initial-mysql.sql
+++ b/share/initial-mysql.sql
@@ -10,9 +10,10 @@ CREATE TABLE test_results (
     domain varchar(255) NOT NULL,
 	batch_id integer NULL,
 	creation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
-	test_start_time TIMESTAMP,
-	test_end_time TIMESTAMP,
+	test_start_time TIMESTAMP NULL,
+	test_end_time TIMESTAMP NULL,
 	priority integer DEFAULT 10,
+	queue integer DEFAULT 0,
 	progress integer DEFAULT 0,
 	params_deterministic_hash character varying(32),
 	params blob NOT NULL,
@@ -20,6 +21,10 @@ CREATE TABLE test_results (
     undelegated boolean NOT NULL DEFAULT false
 ) Engine=InnoDB;
 
+CREATE INDEX test_results__hash_id ON test_results (hash_id);
+CREATE INDEX test_results__params_deterministic_hash ON test_results (params_deterministic_hash);
+CREATE INDEX test_results__batch_id_progress ON test_results (batch_id, progress);
+
 DELIMITER //
 CREATE TRIGGER before_insert_test_results
 	BEFORE INSERT ON test_results
@@ -43,7 +48,7 @@ CREATE TABLE users (
     api_key varchar(512),
 	user_info blob DEFAULT NULL
 ) Engine=InnoDB;
-GRANT SELECT,UPDATE,INSERT ON zonemaster.test_results TO 'zonemaster';
+GRANT ALL ON zonemaster.test_results TO 'zonemaster';
 GRANT LOCK TABLES          ON zonemaster.* TO 'zonemaster';
 GRANT SELECT,UPDATE,INSERT ON zonemaster.batch_jobs TO 'zonemaster';
 GRANT SELECT,UPDATE,INSERT ON zonemaster.users TO 'zonemaster';
diff --git a/docs/initial-postgres.sql b/share/initial-postgres.sql
similarity index 79%
rename from docs/initial-postgres.sql
rename to share/initial-postgres.sql
index f9ed094..dc16ed6 100644
--- a/docs/initial-postgres.sql
+++ b/share/initial-postgres.sql
@@ -12,12 +12,17 @@ CREATE TABLE test_results (
 	test_start_time timestamp without time zone,
 	test_end_time timestamp without time zone,
 	priority integer DEFAULT 10,
+	queue integer DEFAULT 0,
 	progress integer DEFAULT 0,
 	params_deterministic_hash varchar(32),
 	params json NOT NULL,
 	results json
 );
 
+CREATE INDEX test_results__hash_id ON test_results (hash_id);
+CREATE INDEX test_results__params_deterministic_hash ON test_results (params_deterministic_hash);
+CREATE INDEX test_results__batch_id_progress ON test_results (batch_id, progress);
+
 CREATE TABLE batch_jobs (
     id serial PRIMARY KEY,
     username varchar(50) NOT NULL,
diff --git a/share/travis_mysql_backend_config.ini b/share/travis_mysql_backend_config.ini
index a9b7dc2..5c8364a 100644
--- a/share/travis_mysql_backend_config.ini
+++ b/share/travis_mysql_backend_config.ini
@@ -17,8 +17,8 @@ interpreter=perl
 
 [ZONEMASTER]
 max_zonemaster_execution_time=300
-number_of_professes_for_frontend_testing=20
-number_of_professes_for_batch_testing=20
+number_of_processes_for_frontend_testing=20
+number_of_processes_for_batch_testing=20
 #seconds
 
 # This parameter determines if it is possible to get the test results using a incremental 
@@ -33,4 +33,4 @@ force_hash_id_use_in_API_starting_from_id=1
 #maxmind_isp_db_file=
 
 # Requires the GeoIP2::Database::Reader Perl module to be installed
-#maxmind_city_db_file=
\ No newline at end of file
+#maxmind_city_db_file=
diff --git a/share/travis_sqlite_backend_config.ini b/share/travis_sqlite_backend_config.ini
index 80f4ee6..7ee510c 100644
--- a/share/travis_sqlite_backend_config.ini
+++ b/share/travis_sqlite_backend_config.ini
@@ -17,8 +17,8 @@ interpreter=perl
 
 [ZONEMASTER]
 max_zonemaster_execution_time=300
-number_of_professes_for_frontend_testing=20
-number_of_professes_for_batch_testing=20
+number_of_processes_for_frontend_testing=20
+number_of_processes_for_batch_testing=20
 #seconds
 
 [GEOLOCATION]
@@ -26,4 +26,4 @@ number_of_professes_for_batch_testing=20
 #maxmind_isp_db_file=
 
 # Requires the GeoIP2::Database::Reader Perl module to be installed
-#maxmind_city_db_file=
\ No newline at end of file
+#maxmind_city_db_file=
diff --git a/t/test01.t b/t/test01.t
index 930b6de..b295caf 100644
--- a/t/test01.t
+++ b/t/test01.t
@@ -39,7 +39,7 @@ my $frontend_params_1 = {
     profile        => 'default_profile',    # the id if the Test profile listbox
 
     nameservers => [                       # list of the nameserves up to 32
-        { ns => 'ns1.nic.fr', ip => '1.1.1.1' },       # key values pairs representing nameserver => namesterver_ip
+        { ns => 'ns1.nic.fr' },       # key values pairs representing nameserver => namesterver_ip
         { ns => 'ns2.nic.fr', ip => '192.134.4.1' },
     ],
     ds_info => [                                  # list of DS/Digest pairs up to 32
diff --git a/t/test_DB_backend.pl b/t/test_DB_backend.pl
index c066f47..0567494 100644
--- a/t/test_DB_backend.pl
+++ b/t/test_DB_backend.pl
@@ -72,7 +72,7 @@ if ($db_backend eq 'PostgreSQL') {
 	$user_check_query = q/SELECT * FROM users WHERE user_info->>'username' = 'zonemaster_test'/;
 }
 elsif ($db_backend eq 'MySQL') {
-	$user_check_query = q/SELECT * FROM users WHERE user_info like '%zonemaster_test%'/;
+	$user_check_query = q/SELECT * FROM users WHERE username = 'zonemaster_test'/;
 }
 
 ok(
diff --git a/t/test_validate_syntax.t b/t/test_validate_syntax.t
index 4ca9d9e..f31a55a 100644
--- a/t/test_validate_syntax.t
+++ b/t/test_validate_syntax.t
@@ -26,10 +26,6 @@ $frontend_params->{nameservers} = [    # list of the namaserves up to 32
 ];
 
 # domain present?
-$frontend_params->{domain} = '';
-ok( $engine->validate_syntax( $frontend_params )->{status} eq 'nok', 'domain not present' );
-
-# domain present?
 $frontend_params->{domain} = 'afnic.fr';
 ok( $engine->validate_syntax( $frontend_params )->{status} eq 'ok', 'domain present' );
 
@@ -79,19 +75,10 @@ ok( $engine->validate_syntax( $frontend_params )->{status} eq 'nok',
 	encode_utf8( '64 characters long domain label' ) )
 	or diag( $engine->validate_syntax( $frontend_params )->{message} );
 
-# invalid domain characters
-$frontend_params->{domain} = 'test1_.fr';
-ok( $engine->validate_syntax( $frontend_params )->{status} eq 'nok', encode_utf8( 'invalid domain characters' ) )
-	or diag( $engine->validate_syntax( $frontend_params )->{message} );
-
 #TEST NS
 $frontend_params->{domain} = 'afnic.fr';
 $frontend_params->{nameservers}->[0]->{ip} = '1.2.3.4';
 
-# ns present?
-$frontend_params->{nameservers}->[0]->{ns} = '';
-ok( $engine->validate_syntax( $frontend_params )->{status} eq 'nok', 'domain not present' );
-
 # domain present?
 $frontend_params->{nameservers}->[0]->{ns} = 'afnic.fr';
 ok( $engine->validate_syntax( $frontend_params )->{status} eq 'ok', 'domain present' );
@@ -142,11 +129,6 @@ ok( $engine->validate_syntax( $frontend_params )->{status} eq 'nok',
 	encode_utf8( '64 characters long domain label' ) )
 	or diag( $engine->validate_syntax( $frontend_params )->{message} );
 
-# invalid domain characters
-$frontend_params->{nameservers}->[0]->{ns} = 'test1_.fr';
-ok( $engine->validate_syntax( $frontend_params )->{status} eq 'nok', encode_utf8( 'invalid domain characters' ) )
-	or diag( $engine->validate_syntax( $frontend_params )->{message} );
-
 # DELEGATED TEST
 delete( $frontend_params->{nameservers} );
 

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-perl/packages/libzonemaster-webbackend-perl.git



More information about the Pkg-perl-cvs-commits mailing list