Mobrovac has uploaded a new change for review.
https://gerrit.wikimedia.org/r/247274
Change subject: Update to service-template-node v0.2.2
......................................................................
Update to service-template-node v0.2.2
- Logging: log only whitelisted headers for privacy reasons
- Routes: fully-asynchronous route loading
- Bump dependencies' versions
Change-Id: Iffd68d0a2dfc806d471e64535027ee4004a7d4af
---
M .gitignore
M .jshintignore
M .jshintrc
A .travis.yml
A LICENSE
M app.js
M config.dev.yaml
A config.prod.yaml
A dist/init-scripts/systemd.erb
A dist/init-scripts/sysvinit.erb
A dist/init-scripts/upstart.erb
M lib/util.js
M package.json
M routes/citoid.js
A scripts/gen-init-scripts.rb
M static/index.html
M test/features/app/spec.js
M test/utils/assert.js
M test/utils/logStream.js
M test/utils/server.js
20 files changed, 995 insertions(+), 400 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/services/citoid
refs/changes/74/247274/1
diff --git a/.gitignore b/.gitignore
index 5dafb2d..850a956 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
Dockerfile
-*~
-.DS_Store
-*.log
-node_modules
+.idea/
coverage
+config.yaml
+node_modules
+npm-debug.log
diff --git a/.jshintignore b/.jshintignore
index 1c69eee..a4c38eb 100644
--- a/.jshintignore
+++ b/.jshintignore
@@ -1,3 +1,4 @@
coverage
node_modules
test
+static
diff --git a/.jshintrc b/.jshintrc
index f9c3280..258037d 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -1,38 +1,36 @@
{
+ "predef": [
+ "Map",
+ "Set"
+ ],
- "predef": [
- "Map",
- "Set"
- ],
+ // Enforcing
+ "bitwise": true,
+ "eqeqeq": true,
+ "freeze": true,
+ "noarg": true,
+ "nonew": true,
+ "undef": true,
- // Enforcing
- "bitwise": true,
- "eqeqeq": true,
- "freeze": true,
- "noarg": true,
- "nonew": true,
- "undef": true,
+ "curly": true,
+ "newcap": true,
+ "noempty": true,
+ "immed": true,
- "curly": true,
- "newcap": true,
- "noempty": true,
- "immed": true,
+ // Relaxing
+ "laxbreak": true,
+ "sub": true,
+ "boss": true,
+ "eqnull": true,
+ "multistr": true,
+ "loopfunc": true,
- // Relaxing
- "laxbreak": true,
- "sub": true,
- "boss": true,
- "eqnull": true,
- "multistr": true,
- "loopfunc": true,
+ // Invalid options (?)
+ "regexp": false,
+ "trailing": true,
+ "smarttabs": true,
+ "nomen": false,
- // Invalid options (?)
- "regexp": false,
- "trailing": true,
- "smarttabs": true,
- "nomen": false,
-
- "node": true,
- "esnext": true
-
+ "node": true,
+ "esnext": true
}
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..47f2d25
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,7 @@
+language: node_js
+node_js:
+ - "0.10"
+ - "0.11"
+ - "0.12"
+ - "iojs"
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e06d208
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/app.js b/app.js
index b8eb129..89baadd 100644
--- a/app.js
+++ b/app.js
@@ -8,7 +8,6 @@
var bodyParser = require('body-parser');
var fs = BBPromise.promisifyAll(require('fs'));
var sUtil = require('./lib/util');
-var CitoidService = require('./lib/CitoidService');
var packageInfo = require('./package.json');
var yaml = require('js-yaml');
@@ -20,104 +19,105 @@
*/
function initApp(options) {
- // the main application object
- var app = express();
+ // the main application object
+ var app = express();
- // get the options and make them available in the app
- app.logger = options.logger; // the logging device
- app.metrics = options.metrics; // the metrics
- app.conf = options.config; // this app's config options
- app.info = packageInfo; // this app's package info
+ // get the options and make them available in the app
+ app.logger = options.logger; // the logging device
+ app.metrics = options.metrics; // the metrics
+ app.conf = options.config; // this app's config options
+ app.info = packageInfo; // this app's package info
- // ensure some sane defaults
- if(!app.conf.port) { app.conf.port = 1970; }
- if(!app.conf.interface) { app.conf.interface = '0.0.0.0'; }
- if(!app.conf.compression_level) { app.conf.compression_level = 3; }
- if(app.conf.cors === undefined) { app.conf.cors = '*'; }
- if(!app.conf.csp) {
- app.conf.csp =
- "default-src 'self'; object-src 'none'; media-src *;
img-src *; style-src *; frame-ancestors 'self'";
- }
-
- // set outgoing proxy
- if(app.conf.proxy) {
- process.env.HTTP_PROXY = app.conf.proxy;
- if(!app.conf.zoteroUseProxy) {
- // don't use proxy for accessing Zotero unless
specified in settings
- process.env.NO_PROXY = app.conf.zoteroInterface;
- }
- }
-
- // ensure the User-Agent header is set
- if(!app.conf.userAgent) { app.conf.userAgent = 'Citoid/' +
app.info.version; }
-
- // set up the spec
- if(!app.conf.spec) {
- app.conf.spec = __dirname + '/spec.yaml';
- }
- if(app.conf.spec.constructor !== Object) {
- try {
- app.conf.spec =
yaml.safeLoad(fs.readFileSync(app.conf.spec));
- } catch(e) {
- app.logger.log('warn/spec', 'Could not load the spec: '
+ e);
- app.conf.spec = {};
- }
- }
- if(!app.conf.spec.swagger) {
- app.conf.spec.swagger = '2.0';
- }
- if(!app.conf.spec.info) {
- app.conf.spec.info = {
- version: app.info.version,
- title: app.info.name,
- description: app.info.description
- };
- }
- app.conf.spec.info.version = app.info.version;
- if(!app.conf.spec.paths) {
- app.conf.spec.paths = {};
- }
-
- // set the CORS and CSP headers
- app.all('*', function(req, res, next) {
- if(app.conf.cors !== false) {
- res.header("Access-Control-Allow-Origin",
app.conf.cors);
- res.header("Access-Control-Allow-Headers",
"X-Requested-With, Content-Type");
- }
- res.header('X-XSS-Protection', '1; mode=block');
- res.header('X-Content-Type-Options', 'nosniff');
- res.header('X-Frame-Options', 'SAMEORIGIN');
- res.header('Content-Security-Policy', app.conf.csp);
- res.header('X-Content-Security-Policy', app.conf.csp);
- res.header('X-WebKit-CSP', app.conf.csp);
- next();
- });
- // disable the X-Powered-By header
- app.set('x-powered-by', false);
- // disable the ETag header - users should provide them!
- app.set('etag', false);
- // enable compression
- app.use(compression({level: app.conf.compression_level}));
- // use the JSON body parser
- app.use(bodyParser.json());
- // use the application/x-www-form-urlencoded parser
- app.use(bodyParser.urlencoded({extended: true}));
- // set allowed export formats and expected response type
- app.nativeFormats = {
- 'mediawiki':'application/json',
- 'zotero':'application/json',
- 'mwDeprecated':'application/json'
- };
- app.zoteroFormats = {
- 'bibtex':'application/x-bibtex'
- };
- app.formats = Object.assign({}, app.nativeFormats, app.zoteroFormats);
+ // ensure some sane defaults
+ if(!app.conf.port) { app.conf.port = 1970; }
+ if(!app.conf.interface) { app.conf.interface = '0.0.0.0'; }
+ if(!app.conf.compression_level) { app.conf.compression_level = 3; }
+ if(app.conf.cors === undefined) { app.conf.cors = '*'; }
+ if(!app.conf.csp) {
+ app.conf.csp =
+ "default-src 'self'; object-src 'none'; media-src *; img-src *;
style-src *; frame-ancestors 'self'";
+ }
+ if(!app.conf.userAgent) { app.conf.userAgent = 'Citoid/' +
app.info.version; }
- // init the Citoid service object
- app.citoid = new CitoidService(app);
+ // set outgoing proxy
+ if(app.conf.proxy) {
+ process.env.HTTP_PROXY = app.conf.proxy;
+ if(!app.conf.zoteroUseProxy) {
+ // don't use proxy for accessing Zotero unless specified in
settings
+ process.env.NO_PROXY = app.conf.zoteroInterface;
+ }
+ }
- return BBPromise.resolve(app);
+ // set up header whitelisting for logging
+ if(!app.conf.log_header_whitelist) {
+ app.conf.log_header_whitelist = [
+ 'cache-control', 'content-type', 'content-length', 'if-match',
+ 'user-agent', 'x-request-id'
+ ];
+ }
+ app.conf.log_header_whitelist = new RegExp('^(?:' +
app.conf.log_header_whitelist.map(function(item) {
+ return item.trim();
+ }).join('|') + ')$', 'i');
+
+ // set up the spec
+ if(!app.conf.spec) {
+ app.conf.spec = __dirname + '/spec.yaml';
+ }
+ if(app.conf.spec.constructor !== Object) {
+ try {
+ app.conf.spec = yaml.safeLoad(fs.readFileSync(app.conf.spec));
+ } catch(e) {
+ app.logger.log('warn/spec', 'Could not load the spec: ' + e);
+ app.conf.spec = {};
+ }
+ }
+ if(!app.conf.spec.swagger) {
+ app.conf.spec.swagger = '2.0';
+ }
+ if(!app.conf.spec.info) {
+ app.conf.spec.info = {
+ version: app.info.version,
+ title: app.info.name,
+ description: app.info.description
+ };
+ }
+ app.conf.spec.info.version = app.info.version;
+ if(!app.conf.spec.paths) {
+ app.conf.spec.paths = {};
+ }
+
+ // set the CORS and CSP headers
+ app.all('*', function(req, res, next) {
+ if(app.conf.cors !== false) {
+ res.header('access-control-allow-origin', app.conf.cors);
+ res.header('access-control-allow-headers', 'accept,
x-requested-with, content-type');
+ res.header('access-control-expose-headers', 'etag');
+ }
+ res.header('x-xss-protection', '1; mode=block');
+ res.header('x-content-type-options', 'nosniff');
+ res.header('x-frame-options', 'SAMEORIGIN');
+ res.header('content-security-policy', app.conf.csp);
+ res.header('x-content-security-policy', app.conf.csp);
+ res.header('x-webkit-csp', app.conf.csp);
+
+ sUtil.initAndLogRequest(req, app);
+
+ next();
+ });
+
+ // disable the X-Powered-By header
+ app.set('x-powered-by', false);
+ // disable the ETag header - users should provide them!
+ app.set('etag', false);
+ // enable compression
+ app.use(compression({level: app.conf.compression_level}));
+ // use the JSON body parser
+ app.use(bodyParser.json());
+ // use the application/x-www-form-urlencoded parser
+ app.use(bodyParser.urlencoded({extended: true}));
+
+ return BBPromise.resolve(app);
}
@@ -129,40 +129,44 @@
*/
function loadRoutes (app) {
- // get the list of files in routes/
- return fs.readdirAsync(__dirname + '/routes')
- .map(function (fname) {
- // ... and then load each route
- // but only if it's a js file
- if(!/\.js$/.test(fname)) {
- return;
- }
- // import the route file
- var route = require(__dirname + '/routes/' + fname);
- route = route(app);
- // check that the route exports the object we need
- if(route.constructor !== Object || !route.path || !route.router
|| !(route.api_version || route.skip_domain)) {
- throw new TypeError('routes/' + fname + ' does not
export the correct object!');
- }
- // wrap the route handlers with Promise.try() blocks
- sUtil.wrapRouteHandlers(route.router, app);
- // determine the path prefix
- var prefix = '';
- if(!route.skip_domain) {
- prefix = '/:domain/v' + route.api_version;
- }
- // all good, use that route
- app.use(prefix + route.path, route.router);
- }).then(function () {
- // catch errors
- sUtil.setErrorHandler(app);
- // serve static files from static/ - here to allow routes to
take precedence
- app.use(express.static(__dirname + '/static'));
- // route loading is now complete, return the app object
- return BBPromise.resolve(app);
- });
+ // get the list of files in routes/
+ return fs.readdirAsync(__dirname + '/routes').map(function(fname) {
+ return BBPromise.try(function () {
+ // ... and then load each route
+ // but only if it's a js file
+ if(!/\.js$/.test(fname)) {
+ return undefined;
+ }
+ // import the route file
+ var route = require(__dirname + '/routes/' + fname);
+ return route(app);
+ }).then(function (route) {
+ if(route === undefined) {
+ return undefined;
+ }
+ // check that the route exports the object we need
+ if (route.constructor !== Object || !route.path || !route.router
|| !(route.api_version || route.skip_domain)) {
+ throw new TypeError('routes/' + fname + ' does not export the
correct object!');
+ }
+ // wrap the route handlers with Promise.try() blocks
+ sUtil.wrapRouteHandlers(route.router);
+ // determine the path prefix
+ var prefix = '';
+ if(!route.skip_domain) {
+ prefix = '/:domain/v' + route.api_version;
+ }
+ // all good, use that route
+ app.use(prefix + route.path, route.router);
+ });
+ }).then(function () {
+ // catch errors
+ sUtil.setErrorHandler(app);
+ // route loading is now complete, return the app object
+ return BBPromise.resolve(app);
+ });
}
+
/**
* Creates and start the service's web server
@@ -171,23 +175,24 @@
*/
function createServer(app) {
- // return a promise which creates an HTTP server,
- // attaches the app to it, and starts accepting
- // incoming client requests
- var server;
- return new BBPromise(function (resolve) {
- server = http.createServer(app).listen(
- app.conf.port,
- app.conf.interface,
- resolve
- );
- }).then(function () {
- app.logger.log('info',
- 'Worker ' + process.pid + ' listening on ' +
app.conf.interface + ':' + app.conf.port);
- return server;
- });
+ // return a promise which creates an HTTP server,
+ // attaches the app to it, and starts accepting
+ // incoming client requests
+ var server;
+ return new BBPromise(function (resolve) {
+ server = http.createServer(app).listen(
+ app.conf.port,
+ app.conf.interface,
+ resolve
+ );
+ }).then(function () {
+ app.logger.log('info',
+ 'Worker ' + process.pid + ' listening on ' + app.conf.interface +
':' + app.conf.port);
+ return server;
+ });
}
+
/**
* The service's entry point. It takes over the configuration
@@ -197,9 +202,14 @@
*/
module.exports = function(options) {
- return initApp(options)
- .then(loadRoutes)
- .then(createServer);
+ return initApp(options)
+ .then(loadRoutes)
+ .then(function(app) {
+ // serve static files from static/
+ app.use(express.static(__dirname + '/static'));
+ return app;
+ })
+ .then(createServer);
};
diff --git a/config.dev.yaml b/config.dev.yaml
index 65423a9..b3ca3bc 100644
--- a/config.dev.yaml
+++ b/config.dev.yaml
@@ -34,6 +34,9 @@
port: 1970
# IP address to bind to, all IPs by default
# interface: localhost # uncomment to only listen on localhost
+ # more per-service config settings
+ # the location of the spec, defaults to spec.yaml if not specified
+ # spec: ./spec.template.yaml
# allow cross-domain requests to the API (default '*')
cors: '*'
# to disable use:
@@ -41,7 +44,21 @@
# to restrict to a particular domain, use:
# cors: restricted.domain.org
# URL of the outbound proxy to use (complete with protocol)
- # proxy: http://127.0.0.0.1
+ # proxy: http://my.proxy.org:8080
+ # the list of domains for which not to use the proxy defined above
+ # no_proxy_list:
+ # - domain1.com
+ # - domain2.org
+ # the list of incoming request headers that can be logged; if left empty,
+ # the following headers are allowed: cache-control, content-length,
+ # content-type, if-match, user-agent, x-request-id
+ # log_header_whitelist:
+ # - cache-control
+ # - content-length
+ # - content-type
+ # - if-match
+ # - user-agent
+ # - x-request-id
# User-Agent HTTP header to use for requests
userAgent: null
# URL where to contact Zotero
diff --git a/config.prod.yaml b/config.prod.yaml
new file mode 100644
index 0000000..d593c66
--- /dev/null
+++ b/config.prod.yaml
@@ -0,0 +1,35 @@
+# Number of worker processes to spawn.
+# Set to 0 to run everything in a single process without clustering.
+# Use 'ncpu' to run as many workers as there are CPU units
+num_workers: ncpu
+
+# Log error messages and gracefully restart a worker if v8 reports that it
+# uses more heap (note: not RSS) than this many mb.
+worker_heap_limit_mb: 500
+
+# Logger info
+logging:
+ level: warn
+ streams:
+ # Use gelf-stream -> logstash
+ - type: gelf
+ host: logstash1003.eqiad.wmnet
+ port: 12201
+
+# Statsd metrics reporter
+metrics:
+ type: statsd
+ host: statsd.eqiad.wmnet
+ port: 8125
+
+services:
+ - name: service-template-node
+ # a relative path or the name of an npm package, if different from name
+ module: ./app.js
+ # optionally, a version constraint of the npm package
+ # version: ^0.4.0
+ # per-service config
+ conf:
+ port: 6927
+ # interface: localhost # uncomment to only listen on localhost
+ # more per-service config settings
diff --git a/dist/init-scripts/systemd.erb b/dist/init-scripts/systemd.erb
new file mode 100644
index 0000000..78b5be3
--- /dev/null
+++ b/dist/init-scripts/systemd.erb
@@ -0,0 +1,25 @@
+[Unit]
+Description=<%= @description ? @service_name + ' - ' + @description :
@service_name %>
+Documentation=<%= @homepage %>
+After=network.target local-fs.target
+
+[Service]
+Type=simple
+LimitNOFILE=<%= @no_file %>
+PIDFile=%t/<%= @service_name %>.pid
+User=<%= @service_name %>
+Group=<%= @service_name %>
+WorkingDirectory=/srv/deployment/<%= @service_name %>/deploy
+Environment="NODE_PATH='/srv/deployment/<%= @service_name
%>/deploy/node_modules'" "<%= @service_name.gsub(/[^a-z0-9_]/, '_').upcase
%>_PORT=<%= @port %>"
+ExecStart=/usr/bin/nodejs src/server.js -c /etc/<%= @service_name
%>/config.yaml
+Restart=always
+RestartSec=5
+StandardOutput=syslog
+StandardError=syslog
+SyslogIdentifier=<%= @service_name %>
+TimeoutStartSec=5
+TimeoutStopSec=60
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/dist/init-scripts/sysvinit.erb b/dist/init-scripts/sysvinit.erb
new file mode 100644
index 0000000..f889596
--- /dev/null
+++ b/dist/init-scripts/sysvinit.erb
@@ -0,0 +1,172 @@
+#!/bin/sh
+### BEGIN INIT INFO
+# Provides: <%= @service_name %>
+# Required-Start: $local_fs $network $remote_fs $syslog
+# Required-Stop: $local_fs $network $remote_fs $syslog
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: <%= @description || @service_name %>
+# Description: <%= @description ? @service_name + ' - ' + @description :
@service_name %>
+### END INIT INFO
+
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC="<%= @service_name %> service"
+NAME=<%= @service_name %>
+SCRIPT_PATH=/srv/deployment/$NAME/deploy/src/server.js
+DAEMON="/usr/bin/nodejs $SCRIPT_PATH"
+DAEMON_ARGS="-c /etc/$NAME/config.yaml"
+PIDFILE=/var/run/$NAME.pid
+SCRIPTNAME=/etc/init.d/$NAME
+
+# Exit if the package is not installed
+[ -e "$SCRIPT_PATH" ] || exit 0
+
+# Read configuration variable file if it is present
+# NOTE: only the DAEMON_ARGS var should be set in the file if present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+
+# export some variables into the process' environment
+export PORT
+export INTERFACE
+export NODE_PATH=/srv/deployment/$NAME/deploy/node_modules
+export <%= @service_name.gsub(/[^a-z0-9_]/, '_').upcase %>_PORT=<%= @port %>
+
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
+# and status_of_proc is working.
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+ # up the number of fds [sockets] from 1024
+ ulimit -n <%= @no_file %>
+
+ # Return
+ # 0 if daemon has been started
+ # 1 if daemon was already running
+ # 2 if daemon could not be started
+
+ start-stop-daemon --start --quiet --pidfile $PIDFILE -bm \
+ -c $NAME:$NAME --test \
+ --exec /bin/sh -- \
+ -c "$DAEMON $DAEMON_ARGS 2>&1 | logger -i -t $NAME" \
+ || return 1
+ start-stop-daemon --start --quiet --pidfile $PIDFILE -bm \
+ -c <%= @service_name %>:<%= @service_name %> \
+ --exec /bin/sh -- \
+ -c "$DAEMON $DAEMON_ARGS 2>&1 | logger -i -t $NAME" \
+ || return 2
+ echo "Started <%= @service_name %> service on port <%= @port %>"
+
+
+ # Add code here, if necessary, that waits for the process to be ready
+ # to handle requests from services started subsequently which depend
+ # on this one. As a last resort, sleep for some time.
+ sleep 5
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+ # Return
+ # 0 if daemon has been stopped
+ # 1 if daemon was already stopped
+ # 2 if daemon could not be stopped
+ # other if a failure occurred
+ start-stop-daemon --stop --quiet --retry=TERM/60/KILL/5 --pidfile
$PIDFILE --name $NAME
+ RETVAL="$?"
+ [ "$RETVAL" = 2 ] && return 2
+ # Wait for children to finish too if this is a daemon that forks
+ # and if the daemon is only ever run from this initscript.
+ # If the above conditions are not satisfied then add some other code
+ # that waits for the process to drop all resources that could be
+ # needed by services started subsequently. A last resort is to
+ # sleep for some time.
+ start-stop-daemon --stop --quiet --oknodo --retry=0/5/KILL/5 --exec
$DAEMON
+ [ "$?" = 2 ] && return 2
+ # Many daemons don't delete their pidfiles when they exit.
+ rm -f $PIDFILE
+ return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+do_reload() {
+ #
+ # If the daemon can reload its configuration without
+ # restarting (for example, when it is sent a SIGHUP),
+ # then implement that here.
+ #
+ start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name
$NAME
+ return 0
+}
+
+case "$1" in
+ start)
+ [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+ do_start
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ stop)
+ [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ status)
+ status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+ ;;
+ #reload|force-reload)
+ #
+ # If do_reload() is not implemented then leave this commented out
+ # and leave 'force-reload' as an alias for 'restart'.
+ #
+ #log_daemon_msg "Reloading $DESC" "$NAME"
+ #do_reload
+ #log_end_msg $?
+ #;;
+ restart|force-reload)
+ #
+ # If the "reload" option is implemented then remove the
+ # 'force-reload' alias
+ #
+ log_daemon_msg "Restarting $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1)
+ do_start
+ case "$?" in
+ 0) log_end_msg 0 ;;
+ 1) log_end_msg 1 ;; # Old process is still running
+ *) log_end_msg 1 ;; # Failed to start
+ esac
+ ;;
+ *)
+ # Failed to stop
+ log_end_msg 1
+ ;;
+ esac
+ ;;
+ *)
+ echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
+ exit 3
+ ;;
+esac
+
diff --git a/dist/init-scripts/upstart.erb b/dist/init-scripts/upstart.erb
new file mode 100644
index 0000000..4cffcb5
--- /dev/null
+++ b/dist/init-scripts/upstart.erb
@@ -0,0 +1,24 @@
+# Upstart job for <%= @service_name %>
+
+description "<%= @description ? @service_name + ' - ' + @description :
@service_name %>"
+
+start on (local-filesystems and net-device-up IFACE!=lo)
+stop on runlevel [!2345]
+
+# up ulimit -n a bit
+limit nofile <%= @no_file %> <%= @no_file %>
+
+setuid "<%= @service_name %>"
+setgid "<%= @service_name %>"
+
+env NODE_PATH="/srv/deployment/<%= @service_name %>/deploy/node_modules"
+env <%= @service_name.gsub(/[^a-zA-Z0-9_]/, '_').upcase %>_PORT="<%= @port %>"
+
+respawn
+
+# wait 60 seconds for a graceful restart before killing the master
+kill timeout 60
+
+chdir /srv/deployment/<%= @service_name %>/deploy
+exec /usr/bin/nodejs src/server.js -c /etc/<%= @service_name %>/config.yaml >>
/var/log/<%= @service_name %>/main.log 2>&1
+
diff --git a/lib/util.js b/lib/util.js
index c6e4aed..868765e 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -13,29 +13,29 @@
*/
function HTTPError(response) {
- Error.call(this);
- Error.captureStackTrace(this, HTTPError);
+ Error.call(this);
+ Error.captureStackTrace(this, HTTPError);
- if(response.constructor !== Object) {
- // just assume this is just the error message
- var msg = response;
- response = {
- status: 500,
- type: 'internal_error',
- title: 'InternalError',
- detail: msg
- };
- }
+ if(response.constructor !== Object) {
+ // just assume this is just the error message
+ var msg = response;
+ response = {
+ status: 500,
+ type: 'internal_error',
+ title: 'InternalError',
+ detail: msg
+ };
+ }
- this.name = this.constructor.name;
- this.message = response.status + '';
- if(response.type) {
- this.message += ': ' + response.type;
- }
+ this.name = this.constructor.name;
+ this.message = response.status + '';
+ if(response.type) {
+ this.message += ': ' + response.type;
+ }
- for (var key in response) {
- this[key] = response[key];
- }
+ for (var key in response) {
+ this[key] = response[key];
+ }
}
@@ -45,21 +45,32 @@
/**
* Generates an object suitable for logging out of a request object
*
- * @param {Request} the request
+ * @param {Request} req the request
+ * @param {RegExp} whitelistRE the RegExp used to filter headers
* @return {Object} an object containing the key components of the request
*/
-function reqForLog(req) {
+function reqForLog(req, whitelistRE) {
- return {
- url: req.originalUrl,
- headers: req.headers,
- method: req.method,
- params: req.params,
- query: req.query,
- body: req.body,
- remoteAddress: req.connection.remoteAddress,
- remotePort: req.connection.remotePort
- };
+ var ret = {
+ url: req.originalUrl,
+ headers: {},
+ method: req.method,
+ params: req.params,
+ query: req.query,
+ body: req.body,
+ remoteAddress: req.connection.remoteAddress,
+ remotePort: req.connection.remotePort
+ };
+
+ if(req.headers && whitelistRE) {
+ Object.keys(req.headers).forEach(function(hdr) {
+ if(whitelistRE.test(hdr)) {
+ ret.headers[hdr] = req.headers[hdr];
+ }
+ });
+ }
+
+ return ret;
}
@@ -67,17 +78,17 @@
/**
* Serialises an error object in a form suitable for logging
*
- * @param {Error} the error to serialise
+ * @param {Error} err error to serialise
* @return {Object} the serialised version of the error
*/
function errForLog(err) {
- var ret = bunyan.stdSerializers.err(err);
- ret.status = err.status;
- ret.type = err.type;
- ret.detail = err.detail;
+ var ret = bunyan.stdSerializers.err(err);
+ ret.status = err.status;
+ ret.type = err.type;
+ ret.detail = err.detail;
- return ret;
+ return ret;
}
@@ -88,10 +99,9 @@
*/
function generateRequestId() {
- return uuid.TimeUuid.now().toString();
+ return uuid.TimeUuid.now().toString();
}
-
/**
@@ -100,26 +110,21 @@
* regardless of whether a handler returns/uses promises
* or not.
*
- * @param {Router} the router object
- * @param {Application} the application object
+ * @param {Router} router object
*/
-function wrapRouteHandlers(router, app) {
+function wrapRouteHandlers(router) {
- router.stack.forEach(function(routerLayer) {
- routerLayer.route.stack.forEach(function(layer) {
- var origHandler = layer.handle;
- layer.handle = function(req, res, next) {
- BBPromise.try(function() {
- req.headers = req.headers || {};
- req.headers['x-request-id'] =
req.headers['x-request-id'] || generateRequestId();
- req.logger =
app.logger.child({request_id: req.headers['x-request-id']});
- req.logger.log('trace/req', {req:
reqForLog(req), msg: 'incoming request'});
- return origHandler(req, res, next);
- })
- .catch(next);
- };
- });
- });
+ router.stack.forEach(function(routerLayer) {
+ routerLayer.route.stack.forEach(function(layer) {
+ var origHandler = layer.handle;
+ layer.handle = function(req, res, next) {
+ BBPromise.try(function() {
+ return origHandler(req, res, next);
+ })
+ .catch(next);
+ };
+ });
+ });
}
@@ -132,76 +137,76 @@
*/
function setErrorHandler(app) {
- app.use(function(err, req, res, next) {
- var errObj;
- // ensure this is an HTTPError object
- if(err.constructor === HTTPError) {
- errObj = err;
- } else if(err instanceof Error) {
- // is this an HTTPError defined elsewhere? (preq)
- if(err.constructor.name === 'HTTPError') {
- var o = { status: err.status };
- if(err.body && err.body.constructor === Object)
{
-
Object.keys(err.body).forEach(function(key) {
- o[key] = err.body[key];
- });
- } else {
- o.detail = err.body;
- }
- o.message = err.message;
- errObj = new HTTPError(o);
- } else {
- // this is a standard error, convert it
- errObj = new HTTPError({
- status: 500,
- type: 'internal_error',
- title: err.name,
- detail: err.message,
- stack: err.stack
- });
- }
- } else if(err.constructor === Object) {
- // this is a regular object, suppose it's a response
- errObj = new HTTPError(err);
- } else {
- // just assume this is just the error message
- errObj = new HTTPError({
- status: 500,
- type: 'internal_error',
- title: 'InternalError',
- detail: err
- });
- }
- // ensure some important error fields are present
- if(!errObj.status) { errObj.status = 500; }
- if(!errObj.type) { errObj.type = 'internal_error'; }
- // add the offending URI and method as well
- if(!errObj.method) { errObj.method = req.method; }
- if(!errObj.uri) { errObj.uri = req.url; }
- // some set 'message' or 'description' instead of 'detail'
- errObj.detail = errObj.detail || errObj.message ||
errObj.description || '';
- // adjust the log level based on the status code
- var level = 'error';
- if(Number.parseInt(errObj.status) < 400) {
- level = 'trace';
- } else if(Number.parseInt(errObj.status) < 500) {
- level = 'info';
- }
- // log the error
- (req.logger || app.logger).log(level + '/' +
- (errObj.component ? errObj.component :
errObj.status),
- errForLog(errObj));
- // let through only non-sensitive info
- var respBody = {
- status: errObj.status,
- type: errObj.type,
- title: errObj.title,
- detail: errObj.detail,
- method: errObj.method,
- uri: errObj.uri
- };
- res.status(errObj.status).json(respBody);
- });
+ app.use(function(err, req, res, next) {
+ var errObj;
+ // ensure this is an HTTPError object
+ if(err.constructor === HTTPError) {
+ errObj = err;
+ } else if(err instanceof Error) {
+ // is this an HTTPError defined elsewhere? (preq)
+ if(err.constructor.name === 'HTTPError') {
+ var o = { status: err.status };
+ if(err.body && err.body.constructor === Object) {
+ Object.keys(err.body).forEach(function(key) {
+ o[key] = err.body[key];
+ });
+ } else {
+ o.detail = err.body;
+ }
+ o.message = err.message;
+ errObj = new HTTPError(o);
+ } else {
+ // this is a standard error, convert it
+ errObj = new HTTPError({
+ status: 500,
+ type: 'internal_error',
+ title: err.name,
+ detail: err.message,
+ stack: err.stack
+ });
+ }
+ } else if(err.constructor === Object) {
+ // this is a regular object, suppose it's a response
+ errObj = new HTTPError(err);
+ } else {
+ // just assume this is just the error message
+ errObj = new HTTPError({
+ status: 500,
+ type: 'internal_error',
+ title: 'InternalError',
+ detail: err
+ });
+ }
+ // ensure some important error fields are present
+ if(!errObj.status) { errObj.status = 500; }
+ if(!errObj.type) { errObj.type = 'internal_error'; }
+ // add the offending URI and method as well
+ if(!errObj.method) { errObj.method = req.method; }
+ if(!errObj.uri) { errObj.uri = req.url; }
+ // some set 'message' or 'description' instead of 'detail'
+ errObj.detail = errObj.detail || errObj.message || errObj.description
|| '';
+ // adjust the log level based on the status code
+ var level = 'error';
+ if(Number.parseInt(errObj.status) < 400) {
+ level = 'trace';
+ } else if(Number.parseInt(errObj.status) < 500) {
+ level = 'info';
+ }
+ // log the error
+ (req.logger || app.logger).log(level + '/' +
+ (errObj.component ? errObj.component : errObj.status),
+ errForLog(errObj));
+ // let through only non-sensitive info
+ var respBody = {
+ status: errObj.status,
+ type: errObj.type,
+ title: errObj.title,
+ detail: errObj.detail,
+ method: errObj.method,
+ uri: errObj.uri
+ };
+ res.status(errObj.status).json(respBody);
+ });
}
@@ -214,25 +219,40 @@
*/
function createRouter(opts) {
- var options = {
- mergeParams: true
- };
+ var options = {
+ mergeParams: true
+ };
- if(opts && opts.constructor === Object) {
- Object.keys(opts).forEach(function(key) {
- options[key] = opts[key];
- });
- }
+ if(opts && opts.constructor === Object) {
+ Object.keys(opts).forEach(function(key) {
+ options[key] = opts[key];
+ });
+ }
- return express.Router(options);
+ return express.Router(options);
}
+/**
+ * Adds logger to the request and logs it
+ *
+ * @param {*} req request object
+ * @param {Application} app application object
+ */
+function initAndLogRequest(req, app) {
+ req.headers = req.headers || {};
+ req.headers['x-request-id'] = req.headers['x-request-id'] ||
generateRequestId();
+ req.logger = app.logger.child({request_id: req.headers['x-request-id']});
+ req.logger.log('trace/req', {req: reqForLog(req,
app.conf.log_header_whitelist), msg: 'incoming request'});
+}
+
+
module.exports = {
- HTTPError: HTTPError,
- wrapRouteHandlers: wrapRouteHandlers,
- setErrorHandler: setErrorHandler,
- router: createRouter
+ HTTPError: HTTPError,
+ initAndLogRequest: initAndLogRequest,
+ wrapRouteHandlers: wrapRouteHandlers,
+ setErrorHandler: setErrorHandler,
+ router: createRouter
};
diff --git a/package.json b/package.json
index 8642adf..bbb0c94 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,8 @@
"version": "0.3.1",
"description": "Converts search terms such as URL or DOI into citations.",
"homepage": "https://www.mediawiki.org/wiki/Citoid",
+ "license": "Apache-2.0",
+ "main": "./app.js",
"scripts": {
"start": "service-runner",
"test": "mocha test/index.js",
@@ -10,30 +12,30 @@
},
"dependencies": {
"bluebird": "2.8.2",
- "body-parser": "1.13.3",
+ "body-parser": "1.14.1",
"bunyan": "1.5.1",
"cassandra-uuid": "0.0.2",
"cheerio": "0.19.0",
- "compression": "1.5.2",
+ "compression": "1.6.0",
"content-type": "1.0.1",
"express": "4.13.3",
"html-metadata": "1.2.1",
"iconv-lite": "0.4.11",
"ip": "1.0.1",
- "js-yaml": "3.4.2",
+ "js-yaml": "3.4.3",
"preq": "0.4.4",
"request": "^2.58.0",
- "service-runner": "0.2.5",
+ "service-runner": "0.2.11",
"tough-cookie": "2.0.0",
"striptags": "2.0.3"
},
"devDependencies": {
"extend": "3.0.0",
- "istanbul": "0.3.17",
- "mocha": "2.2.5",
+ "istanbul": "0.3.22",
+ "mocha": "2.3.3",
"mocha-jshint": "2.2.3",
- "mocha-lcov-reporter": "0.0.2",
- "swagger-router": "0.1.1"
+ "mocha-lcov-reporter": "1.0.0",
+ "swagger-router": "0.2.0"
},
"deploy": {
"target": "ubuntu",
diff --git a/routes/citoid.js b/routes/citoid.js
index 3f33db4..8f01279 100644
--- a/routes/citoid.js
+++ b/routes/citoid.js
@@ -3,6 +3,7 @@
var sUtil = require('../lib/util');
var CitoidRequest = require('../lib/CitoidRequest.js');
+var CitoidService = require('../lib/CitoidService');
/**
* The main router object
@@ -95,6 +96,20 @@
app = appObj;
+ // set allowed export formats and expected response type
+ app.nativeFormats = {
+ 'mediawiki':'application/json',
+ 'zotero':'application/json',
+ 'mwDeprecated':'application/json'
+ };
+ app.zoteroFormats = {
+ 'bibtex':'application/x-bibtex'
+ };
+ app.formats = Object.assign({}, app.nativeFormats, app.zoteroFormats);
+
+ // init the Citoid service object
+ app.citoid = new CitoidService(app);
+
return {
path: '/',
skip_domain: true,
diff --git a/scripts/gen-init-scripts.rb b/scripts/gen-init-scripts.rb
new file mode 100755
index 0000000..e523b97
--- /dev/null
+++ b/scripts/gen-init-scripts.rb
@@ -0,0 +1,71 @@
+#!/usr/bin/env ruby
+
+
+require 'erb'
+require 'json'
+require 'yaml'
+
+
+rootdir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
+indir = File.join(rootdir, 'dist', 'init-scripts')
+outdir = indir
+
+
+class ScriptData
+
+ include ERB::Util
+
+ @@suffix = {'systemd' => '.service', 'upstart' => '.conf'}
+
+ def initialize input_dir
+ @template = {}
+ self.init input_dir
+ end
+
+ def set_info root_dir
+ self.read_info(root_dir).each do |key, value|
+ self.instance_variable_set "@#{key}".to_sym, value
+ end
+ @service_name = @name
+ @no_file ||= 10000
+ end
+
+ def generate output_dir
+ @template.each do |name, erb|
+ File.open(File.join(output_dir, "#{@name}#{@@suffix[name]}"), 'w') do
|io|
+ io.write erb.result(binding())
+ end
+ end
+ end
+
+ def init input_dir
+ Dir.glob(File.join(input_dir, '*.erb')).each do |fname|
+ @template[File.basename(fname, '.erb')] = ERB.new(File.read(fname))
+ end
+ end
+
+ def read_info root_dir
+ data = YAML.load(File.read(File.join(root_dir,
'config.yaml')))['services'][0]['conf']
+ return data.merge(JSON.load(File.read(File.join(root_dir,
'package.json'))))
+ end
+
+end
+
+
+if ARGV.size > 0 and ['-h', '--help'].include? ARGV[0]
+ puts 'This is a simple script to generate various service init scripts'
+ puts 'Usage: gen-init-scripts.rb [output_dir]'
+ exit 1
+elsif ARGV.size > 0
+ outdir = ARGV[0]
+end
+
+unless File.directory? outdir
+ STDERR.puts 'The output directory must exist! Aborting...'
+ exit 2
+end
+
+data = ScriptData.new indir
+data.set_info rootdir
+data.generate outdir
+
diff --git a/static/index.html b/static/index.html
index 3b0f8db..c0ae1c8 100644
--- a/static/index.html
+++ b/static/index.html
@@ -12,4 +12,4 @@
<input type="hidden" name="format" value="mediawiki" />
<p>URL: <input name="search" size="100"
value="http://link.springer.com/chapter/10.1007/11926078_68" /> <input
type="submit" /></p>
</form>
-</body></html>
\ No newline at end of file
+</body></html>
diff --git a/test/features/app/spec.js b/test/features/app/spec.js
index 9c685fa..570fb40 100644
--- a/test/features/app/spec.js
+++ b/test/features/app/spec.js
@@ -1,12 +1,12 @@
'use strict';
-var preq = require('preq');
+var preq = require('preq');
var assert = require('../../utils/assert.js');
var server = require('../../utils/server.js');
-var URI = require('swagger-router').URI;
-var yaml = require('js-yaml');
-var fs = require('fs');
+var URI = require('swagger-router').URI;
+var yaml = require('js-yaml');
+var fs = require('fs');
function staticSpecLoad() {
diff --git a/test/utils/assert.js b/test/utils/assert.js
index 644d60a..73871e4 100644
--- a/test/utils/assert.js
+++ b/test/utils/assert.js
@@ -149,15 +149,14 @@
}
-module.exports.ok = assert.ok;
-module.exports.fails = fails;
-module.exports.deepEqual = deepEqual;
-module.exports.isDeepEqual = isDeepEqual;
-module.exports.notDeepEqual = notDeepEqual;
-module.exports.contentType = contentType;
-module.exports.status = status;
+module.exports.ok = assert.ok;
+module.exports.fails = fails;
+module.exports.deepEqual = deepEqual;
+module.exports.isDeepEqual = isDeepEqual;
+module.exports.notDeepEqual = notDeepEqual;
+module.exports.contentType = contentType;
+module.exports.status = status;
module.exports.checkError = checkError;
module.exports.checkCitation = checkCitation;
module.exports.checkZotCitation = checkZotCitation;
module.exports.checkBibtex = checkBibtex;
-
diff --git a/test/utils/logStream.js b/test/utils/logStream.js
index 2440e15..f8e292d 100644
--- a/test/utils/logStream.js
+++ b/test/utils/logStream.js
@@ -1,72 +1,68 @@
'use strict';
-
var bunyan = require('bunyan');
-
function logStream(logStdout) {
- var log = [];
- var parrot = bunyan.createLogger({
- name: 'test-logger',
- level: 'warn'
- });
+ var log = [];
+ var parrot = bunyan.createLogger({
+ name: 'test-logger',
+ level: 'warn'
+ });
- function write(chunk, encoding, callback) {
- try {
- var entry = JSON.parse(chunk);
- var levelMatch = /^(\w+)/.exec(entry.levelPath);
- if (logStdout && levelMatch) {
- var level = levelMatch[1];
- if (parrot[level]) {
- parrot[level](entry);
- }
+ function write(chunk, encoding, callback) {
+ try {
+ var entry = JSON.parse(chunk);
+ var levelMatch = /^(\w+)/.exec(entry.levelPath);
+ if (logStdout && levelMatch) {
+ var level = levelMatch[1];
+ if (parrot[level]) {
+ parrot[level](entry);
}
- } catch (e) {
- console.error('something went wrong trying to parrot a log entry',
e, chunk);
}
-
- log.push(chunk);
+ } catch (e) {
+ console.error('something went wrong trying to parrot a log entry', e,
chunk);
}
- // to implement the stream writer interface
- function end(chunk, encoding, callback) {
+ log.push(chunk);
+ }
+
+ // to implement the stream writer interface
+ function end(chunk, encoding, callback) {
+ }
+
+ function get() {
+ return log;
+ }
+
+ function slice() {
+
+ var begin = log.length;
+ var end = null;
+
+ function halt() {
+ if (end === null) {
+ end = log.length;
+ }
}
function get() {
- return log;
- }
-
- function slice() {
-
- var begin = log.length;
- var end = null;
-
- function halt() {
- if (end === null) {
- end = log.length;
- }
- }
-
- function get() {
- return log.slice(begin, end);
- }
-
- return {
- halt: halt,
- get: get
- };
-
+ return log.slice(begin, end);
}
return {
- write: write,
- end: end,
- slice: slice,
- get: get
+ halt: halt,
+ get: get
};
-}
+ }
+ return {
+ write: write,
+ end: end,
+ slice: slice,
+ get: get
+ };
+}
module.exports = logStream;
diff --git a/test/utils/server.js b/test/utils/server.js
index b811768..15576b4 100644
--- a/test/utils/server.js
+++ b/test/utils/server.js
@@ -1,6 +1,10 @@
'use strict';
+// mocha defines to avoid JSHint breakage
+/* global describe, it, before, beforeEach, after, afterEach */
+
+
var BBPromise = require('bluebird');
var ServiceRunner = require('service-runner');
var logStream = require('./logStream');
@@ -32,9 +36,9 @@
// make a deep copy of it for later reference
var origConfig = extend(true, {}, config);
-var stop = function () {};
+var stop = function () {};
var options = null;
-var runner = new ServiceRunner();
+var runner = new ServiceRunner();
function start(_options) {
@@ -88,9 +92,6 @@
}
-module.exports = {
- config: config,
- start: start,
- query: query
-};
-
+module.exports.config = config;
+module.exports.start = start;
+module.exports.query = query;
--
To view, visit https://gerrit.wikimedia.org/r/247274
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Iffd68d0a2dfc806d471e64535027ee4004a7d4af
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/services/citoid
Gerrit-Branch: master
Gerrit-Owner: Mobrovac <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits