├── views ├── error.jade ├── layout.jade ├── validate.jade └── index.jade ├── Dockerfile ├── .gitignore ├── public └── stylesheets │ └── style.css ├── routes ├── index.js └── validate.js ├── lib ├── note.js └── validator.js ├── package.json ├── README.md ├── LICENSE.md ├── app.js └── bin └── www /views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:6 2 | 3 | WORKDIR /opt/validator 4 | ADD . . 5 | 6 | RUN npm install 7 | 8 | EXPOSE 80 9 | EXPOSE 443 10 | CMD ["npm", "start"] 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | /* activitystreams-validator: https://github.com/w3c-social/activitystreams-validator 2 | * 3 | * Copyright © 2016 World Wide Web Consortium, (Massachusetts Institute of 4 | * Technology, European Research Consortium for Informatics and Mathematics, 5 | * Keio University, Beihang). All Rights Reserved. This work is distributed 6 | * under the W3C® Software License [1] in the hope that it will be useful, but 7 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 8 | * FITNESS FOR A PARTICULAR PURPOSE. 9 | * 10 | * [1] http://www.w3.org/Consortium/Legal/copyright-software 11 | */ 12 | 13 | body { 14 | padding: 50px; 15 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 16 | } 17 | 18 | a { 19 | color: #00B7FF; 20 | } 21 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | // activitystreams-validator: https://github.com/w3c-social/activitystreams-validator 2 | // 3 | // Copyright © 2016 World Wide Web Consortium, (Massachusetts Institute of 4 | // Technology, European Research Consortium for Informatics and Mathematics, 5 | // Keio University, Beihang). All Rights Reserved. This work is distributed 6 | // under the W3C® Software License [1] in the hope that it will be useful, but 7 | // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 8 | // FITNESS FOR A PARTICULAR PURPOSE. 9 | // 10 | // [1] http://www.w3.org/Consortium/Legal/copyright-software 11 | 12 | var express = require('express'); 13 | var router = express.Router(); 14 | 15 | /* GET home page. */ 16 | router.get('/', function(req, res, next) { 17 | res.render('index', { title: 'Activity Streams 2.0 Validator' }); 18 | }); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css', integrity='sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7', crossorigin='anonymous') 7 | link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css', integrity='sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r', crossorigin='anonymous') 8 | 9 | body 10 | block content 11 | script(src='https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js') 12 | script(src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js', integrity='sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS', crossorigin='anonymous') 13 | -------------------------------------------------------------------------------- /lib/note.js: -------------------------------------------------------------------------------- 1 | // activitystreams-validator: https://github.com/w3c-social/activitystreams-validator 2 | // 3 | // Copyright © 2016 World Wide Web Consortium, (Massachusetts Institute of 4 | // Technology, European Research Consortium for Informatics and Mathematics, 5 | // Keio University, Beihang). All Rights Reserved. This work is distributed 6 | // under the W3C® Software License [1] in the hope that it will be useful, but 7 | // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 8 | // FITNESS FOR A PARTICULAR PURPOSE. 9 | // 10 | // [1] http://www.w3.org/Consortium/Legal/copyright-software 11 | 12 | var Note = function(level, path, text) { 13 | this.level = level; 14 | this.path = path; 15 | this.text = text; 16 | }; 17 | 18 | Note.ERROR = "error"; 19 | Note.WARNING = "warning"; 20 | Note.NOTICE = "notice"; 21 | Note.INFO = "info"; 22 | 23 | module.exports = Note; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "activitystreams-validator", 3 | "version": "0.4.5", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www", 7 | "docker": "docker build -t w3csocial/activitystreams-validator:${npm_package_version} . && docker tag w3csocial/activitystreams-validator:${npm_package_version} w3csocial/activitystreams-validator:latest", 8 | "push": "docker push w3csocial/activitystreams-validator:${npm_package_version} && docker push w3csocial/activitystreams-validator:latest" 9 | }, 10 | "dependencies": { 11 | "body-parser": "~1.13.2", 12 | "cookie-parser": "~1.3.5", 13 | "debug": "~2.2.0", 14 | "express": "~4.13.1", 15 | "jade": "~1.11.0", 16 | "morgan": "~1.6.1", 17 | "serve-favicon": "~2.3.0", 18 | "multer": "^1.1.0", 19 | "activitystrea.ms": "^0.14.0", 20 | "request": "^2.69.0", 21 | "lodash": "^4.5.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /views/validate.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1 Validation results 5 | 6 | if typeof(url) !== 'undefined' 7 | p 8 | a(href=url) #{url} 9 | 10 | form(id='validateByData', method='POST', action='/validate') 11 | .form-group 12 | label(for='data') Data 13 | textarea#data.form-control(rows='8', name='data') #{input} 14 | button.btn.btn-default(type='submit') Re-submit 15 | 16 | h2 Notes 17 | table.table 18 | tbody 19 | tr 20 | th Level 21 | th Path 22 | th Note 23 | 24 | each note in notes 25 | tr(class=(note.level=="error"?"danger":note.level)) 26 | td= note.level 27 | td= "/" + note.path.join("/") 28 | td= note.text 29 | 30 | p 31 | | Working AS2 publisher? Submit an  32 | a(href='https://github.com/w3c/activitystreams/tree/master/implementation-reports') 33 | implementation report 34 | | to help with development of the standard. 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | activitystreams-validator 2 | ========================= 3 | 4 | validator for AS2 URLs and uploaded data 5 | 6 | License 7 | ------- 8 | 9 | Copyright © 2016 World Wide Web Consortium, (Massachusetts Institute of 10 | Technology, European Research Consortium for Informatics and Mathematics, Keio 11 | University, Beihang). All Rights Reserved. This work is distributed under the 12 | [W3C® Software License](http://www.w3.org/Consortium/Legal/copyright-software) 13 | in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the 14 | implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | 16 | Installation 17 | ------------ 18 | 19 | There was a live version available at as2.rocks until late 2018. 20 | 21 | Please feel free to setup your own instance and submit a patch here to link to it! 22 | 23 | If you want to run your own, there is a Docker container: 24 | 25 | https://hub.docker.com/r/w3csocial/activitystreams-validator/ 26 | 27 | If you just want to run from your own server, you should be able to do this in 28 | your working directory: 29 | 30 | ``` 31 | npm install 32 | npm start 33 | ``` 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This work is being provided by the copyright holders under the following 2 | license. 3 | 4 | License 5 | ------- 6 | 7 | By obtaining and/or copying this work, you (the licensee) agree that you have 8 | read, understood, and will comply with the following terms and conditions. 9 | 10 | Permission to copy, modify, and distribute this work, with or without 11 | modification, for any purpose and without fee or royalty is hereby granted, 12 | provided that you include the following on ALL copies of the work or portions 13 | thereof, including modifications: 14 | 15 | * The full text of this NOTICE in a location viewable to users of the 16 | redistributed or derivative work. 17 | 18 | * Any pre-existing intellectual property disclaimers, notices, or terms and 19 | conditions. If none exist, the W3C Software and Document Short Notice should 20 | be included. 21 | 22 | * Notice of any changes or modifications, through a copyright statement on the 23 | new code or document such as "This software or document includes material 24 | copied from or derived from [title and URI of the W3C document]. 25 | Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." 26 | 27 | Disclaimers 28 | ----------- 29 | 30 | THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR 31 | WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF 32 | MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE 33 | SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, 34 | TRADEMARKS OR OTHER RIGHTS. 35 | 36 | COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR 37 | CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. 38 | 39 | The name and trademarks of copyright holders may NOT be used in advertising or 40 | publicity pertaining to the work without specific, written prior permission. 41 | Title to copyright in this work will at all times remain with copyright holders. 42 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p This service lets you validate your Activity Streams 2.0 (AS2) data. 6 | 7 | form(id='validateByUrl', method='GET', action='/validate') 8 | .form-group 9 | label(for='url') URL 10 | input#url.form-control(type='url', name='url', placeholder='URL') 11 | p.help-block This is the URL of an Activity Streams 2.0 document. 12 | button.btn.btn-default(type='submit') Validate 13 | 14 | form(id='validateByData', method='POST', action='/validate') 15 | .form-group 16 | label(for='data') Data 17 | textarea#data.form-control(rows='8', name='data') 18 | p.help-block If you just need to validate some data, put it here. 19 | button.btn.btn-default(type='submit') Validate 20 | 21 | form(id='validateByUpload', method='POST', action='/validate', enctype='multipart/form-data') 22 | .form-group 23 | label(for='file') File 24 | input#file.form-control(type='file', name='file', placeholder='URL') 25 | p.help-block You can upload a file here. 26 | button.btn.btn-default(type='submit') Validate 27 | 28 | p 29 | | You can also use the /validate endpoint to send data from the command line 30 | | or your favourite programming language. For example: 31 | blockquote 32 | code 33 | | curl -H "Content-Type: application/activity+json" --data @yourfile.json https://as2.rocks/validate 34 | 35 | p 36 | | This validator is Open Source software, available under the  37 | a(href='https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document', rel='license') 38 | W3C Software and Document Notice and License 39 | | . 40 | | Code is available at  41 | a(href='https://github.com/w3c-social/activitystreams-validator') https://github.com/w3c-social/activitystreams-validator 42 | | . 43 | 44 | p 45 | | To test an Activity Streams 2.0 consumer, use the  46 | a(href='https://github.com/w3c/activitystreams/tree/master/test') 47 | test suite 48 | |  available in the AS2 github repository. 49 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // activitystreams-validator: https://github.com/w3c-social/activitystreams-validator 2 | // 3 | // Copyright © 2016 World Wide Web Consortium, (Massachusetts Institute of 4 | // Technology, European Research Consortium for Informatics and Mathematics, 5 | // Keio University, Beihang). All Rights Reserved. This work is distributed 6 | // under the W3C® Software License [1] in the hope that it will be useful, but 7 | // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 8 | // FITNESS FOR A PARTICULAR PURPOSE. 9 | // 10 | // [1] http://www.w3.org/Consortium/Legal/copyright-software 11 | 12 | var express = require('express'); 13 | var path = require('path'); 14 | var favicon = require('serve-favicon'); 15 | var logger = require('morgan'); 16 | var cookieParser = require('cookie-parser'); 17 | var bodyParser = require('body-parser'); 18 | 19 | var routes = require('./routes/index'); 20 | var validate = require('./routes/validate'); 21 | 22 | var app = express(); 23 | 24 | // view engine setup 25 | app.set('views', path.join(__dirname, 'views')); 26 | app.set('view engine', 'jade'); 27 | 28 | // uncomment after placing your favicon in /public 29 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 30 | app.use(logger('dev')); 31 | app.use(bodyParser.json()); 32 | app.use(bodyParser.json({"type": "application/activity+json"})); 33 | app.use(bodyParser.json({"type": "application/ld+json"})); 34 | app.use(bodyParser.urlencoded({ extended: false })); 35 | app.use(cookieParser()); 36 | app.use(express.static(path.join(__dirname, 'public'))); 37 | 38 | app.use('/', routes); 39 | app.use('/validate', validate); 40 | 41 | // catch 404 and forward to error handler 42 | app.use(function(req, res, next) { 43 | var err = new Error('Not Found'); 44 | err.status = 404; 45 | next(err); 46 | }); 47 | 48 | // error handlers 49 | 50 | // development error handler 51 | // will print stacktrace 52 | if (app.get('env') === 'development') { 53 | app.use(function(err, req, res, next) { 54 | res.status(err.status || 500); 55 | res.render('error', { 56 | message: err.message, 57 | error: err 58 | }); 59 | }); 60 | } 61 | 62 | // production error handler 63 | // no stacktraces leaked to user 64 | app.use(function(err, req, res, next) { 65 | res.status(err.status || 500); 66 | res.render('error', { 67 | message: err.message, 68 | error: {} 69 | }); 70 | }); 71 | 72 | 73 | module.exports = app; 74 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // activitystreams-validator: https://github.com/w3c-social/activitystreams-validator 4 | // 5 | // Copyright © 2016 World Wide Web Consortium, (Massachusetts Institute of 6 | // Technology, European Research Consortium for Informatics and Mathematics, 7 | // Keio University, Beihang). All Rights Reserved. This work is distributed 8 | // under the W3C® Software License [1] in the hope that it will be useful, but 9 | // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 10 | // FITNESS FOR A PARTICULAR PURPOSE. 11 | // 12 | // [1] http://www.w3.org/Consortium/Legal/copyright-software 13 | 14 | /** 15 | * Module dependencies. 16 | */ 17 | 18 | var app = require('../app'); 19 | var debug = require('debug')('activitystreams-validator:server'); 20 | var http = require('http'); 21 | var https = require('https'); 22 | 23 | /** 24 | * Get port from environment and store in Express. 25 | */ 26 | 27 | var port = normalizePort(process.env.PORT || '3000'); 28 | app.set('port', port); 29 | 30 | /** 31 | * Create HTTP or HTTPS server. 32 | */ 33 | 34 | var server = null; 35 | 36 | if (process.env.KEY) { 37 | server = https.createServer({key: process.env.KEY, cert: process.env.CERT}, app); 38 | } else { 39 | server = http.createServer(app); 40 | } 41 | 42 | /** 43 | * Listen on provided port, on all network interfaces. 44 | */ 45 | 46 | server.listen(port); 47 | server.on('error', onError); 48 | server.on('listening', onListening); 49 | 50 | /** 51 | * Normalize a port into a number, string, or false. 52 | */ 53 | 54 | function normalizePort(val) { 55 | var port = parseInt(val, 10); 56 | 57 | if (isNaN(port)) { 58 | // named pipe 59 | return val; 60 | } 61 | 62 | if (port >= 0) { 63 | // port number 64 | return port; 65 | } 66 | 67 | return false; 68 | } 69 | 70 | /** 71 | * Event listener for HTTP server "error" event. 72 | */ 73 | 74 | function onError(error) { 75 | if (error.syscall !== 'listen') { 76 | throw error; 77 | } 78 | 79 | var bind = typeof port === 'string' 80 | ? 'Pipe ' + port 81 | : 'Port ' + port; 82 | 83 | // handle specific listen errors with friendly messages 84 | switch (error.code) { 85 | case 'EACCES': 86 | console.error(bind + ' requires elevated privileges'); 87 | process.exit(1); 88 | break; 89 | case 'EADDRINUSE': 90 | console.error(bind + ' is already in use'); 91 | process.exit(1); 92 | break; 93 | default: 94 | throw error; 95 | } 96 | } 97 | 98 | /** 99 | * Event listener for HTTP server "listening" event. 100 | */ 101 | 102 | function onListening() { 103 | var addr = server.address(); 104 | var bind = typeof addr === 'string' 105 | ? 'pipe ' + addr 106 | : 'port ' + addr.port; 107 | debug('Listening on ' + bind); 108 | } 109 | -------------------------------------------------------------------------------- /routes/validate.js: -------------------------------------------------------------------------------- 1 | // activitystreams-validator: https://github.com/w3c-social/activitystreams-validator 2 | // 3 | // Copyright © 2016 World Wide Web Consortium, (Massachusetts Institute of 4 | // Technology, European Research Consortium for Informatics and Mathematics, 5 | // Keio University, Beihang). All Rights Reserved. This work is distributed 6 | // under the W3C® Software License [1] in the hope that it will be useful, but 7 | // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 8 | // FITNESS FOR A PARTICULAR PURPOSE. 9 | // 10 | // [1] http://www.w3.org/Consortium/Legal/copyright-software 11 | 12 | var fs = require('fs'); 13 | 14 | var express = require('express'); 15 | var upload = require('multer')({dest: process.env['UPLOADS'] || '/tmp/uploads'}); 16 | var as = require('activitystrea.ms'); 17 | var request = require('request'); 18 | 19 | var Validator = require('../lib/validator'); 20 | 21 | var router = express.Router(); 22 | 23 | router.get('/', function(req, res, next) { 24 | var options; 25 | if (!req.query || !req.query.url) { 26 | next(new Error("No URL provided")); 27 | } 28 | url = req.query.url; 29 | options = { 30 | url: req.query.url, 31 | headers: { 32 | 'accept': 'application/activity+json;q=1.0,application/ld+json;q=0.8,application/json;q=0.6,*/*;q=0.1' 33 | } 34 | }; 35 | request.get(options, function(err, response, body) { 36 | if (err) { 37 | next(err); 38 | } else if (response.statusCode != 200) { 39 | next(new Error("Unexpected status code" + response.statusCode)); 40 | } else { 41 | val = new Validator(); 42 | val.validateHTTPResponse(url, response); 43 | val.validateData(body); 44 | res.render("validate", {title: "Validation Report", input: body, notes: val.getNotes()}); 45 | } 46 | }); 47 | }); 48 | 49 | /* Validate input, print some output */ 50 | 51 | router.post('/', function(req, res, next) { 52 | var val; 53 | if (req.is('json') || req.is('application/activity+json') || req.is('application/ld+json')) { 54 | val = new Validator(); 55 | val.validateTopLevelItem(req.body); 56 | res.json({title: "Validation Report", input: req.body, notes: val.getNotes()}); 57 | } else if (req.is('urlencoded')) { 58 | if (!req.body || !req.body.data) { 59 | return next(new Error("No data")); 60 | } 61 | val = new Validator(); 62 | val.validateData(req.body.data); 63 | res.render("validate", {title: "Validation Report", input: req.body.data, notes: val.getNotes()}); 64 | } else if (req.is('multipart/form-data')) { 65 | upload.single('file')(req, res, function(err) { 66 | if (err) { 67 | return next(err); 68 | } else { 69 | fs.readFile(req.file.path, "utf8", function(err, data) { 70 | if (err) { 71 | return next(err); 72 | } else { 73 | val = new Validator(); 74 | val.validateData(data); 75 | res.render("validate", {title: "Validation Report", input: data, notes: val.getNotes()}); 76 | } 77 | }); 78 | } 79 | }); 80 | } else { 81 | next(new Error("Unexpected POST request type")); 82 | } 83 | }); 84 | 85 | module.exports = router; 86 | -------------------------------------------------------------------------------- /lib/validator.js: -------------------------------------------------------------------------------- 1 | // activitystreams-validator: https://github.com/w3c-social/activitystreams-validator 2 | // 3 | // Copyright © 2016 World Wide Web Consortium, (Massachusetts Institute of 4 | // Technology, European Research Consortium for Informatics and Mathematics, 5 | // Keio University, Beihang). All Rights Reserved. This work is distributed 6 | // under the W3C® Software License [1] in the hope that it will be useful, but 7 | // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 8 | // FITNESS FOR A PARTICULAR PURPOSE. 9 | // 10 | // [1] http://www.w3.org/Consortium/Legal/copyright-software 11 | 12 | var _ = require("lodash"); 13 | 14 | var Note = require('../lib/note'); 15 | 16 | var BASE_URI = "http://www.w3.org/ns/activitystreams#"; 17 | 18 | var contexts = [ 19 | "http://www.w3.org/ns/activitystreams", 20 | "https://www.w3.org/ns/activitystreams", 21 | "http://www.w3.org/ns/activitystreams#", 22 | "https://www.w3.org/ns/activitystreams#" 23 | ]; 24 | 25 | var Validator = function() { 26 | 27 | notes = []; 28 | 29 | canonicalType = function(type) { 30 | if (type.indexOf(BASE_URI) === 0) { 31 | return type.substr(BASE_URI.length); 32 | } else { 33 | return type; 34 | } 35 | }; 36 | 37 | validateJSONType = function(value) { // Check for type 38 | if (_.isNumber(value)) { 39 | notes.push(new Note(Note.ERROR, [], "Top-level is a number; must be an object")); 40 | return true; 41 | } else if (_.isString(value)) { 42 | notes.push(new Note(Note.ERROR, [], "Top-level is a string; must be an object")); 43 | return true; 44 | } else if (_.isArray(value)) { 45 | notes.push(new Note(Note.ERROR, [], "Top-level is an array; must be an object")); 46 | return true; 47 | } else { 48 | return false; 49 | } 50 | }; 51 | 52 | validateContext = function(obj) { 53 | if (!_.has(obj, "@context")) { 54 | notes.push(new Note(Note.WARNING, [], "Top-level does not contain a '@context' property")); 55 | } else if (_.isString(obj["@context"])) { 56 | if (contexts.indexOf(obj["@context"]) == -1) { 57 | notes.push(new Note(Note.WARNING, [], "'@context' property does not refer to Activity Streams context")); 58 | } 59 | } else if (_.isObject(obj["@context"])) { 60 | // FIXME: test to see if the AS2 context is referenced in the @context tree 61 | } else { 62 | notes.push(new Note(Note.ERROR, [], "'@context' property is neither a string nor an object")); 63 | } 64 | }; 65 | 66 | validateStringProperty = function(obj, path, property) { 67 | if (!_.isUndefined(obj[property]) && !_.isString(obj[property])) { 68 | notes.push(new Note(Note.ERROR, path, "'"+property+"' must be a string.")); 69 | } 70 | }; 71 | 72 | validateNonnegativeIntegerProperty = function(obj, path, property) { 73 | if (!_.isUndefined(obj[property]) && !_.isNumber(obj[property])) { 74 | // XXX: check for non-integers 75 | notes.push(new Note(Note.ERROR, path, "'"+property+"' must be an integer.")); 76 | } else if (obj[property] < 0) { 77 | // XXX: check for non-integers 78 | notes.push(new Note(Note.ERROR, path, "'"+property+"' must not be negative.")); 79 | } 80 | }; 81 | 82 | validateIRI = function(item, path, name) { 83 | // FIXME: validate that item is an IRI 84 | }; 85 | 86 | validateObjectProperty = function(obj, path, property) { 87 | validateScalar = function(item, path, name) { 88 | if (!!_.isUndefined(item) || _.isNull(item)) { 89 | return; 90 | } else if (_.isString(item)) { 91 | validateIRI(item, path, name); 92 | } else if (_.isObject(item)) { 93 | validateItem(item, path); 94 | } else { 95 | notes.push(new Note(Note.ERROR, path, "'"+name+"' must be an IRI string, an object, or an array of IRI strings and objects.")); 96 | } 97 | }; 98 | 99 | if (_.isArray(obj[property])) { 100 | if (obj[property].length === 0) { 101 | notes.push(new Note(Note.ERROR, path, "Empty list is not allowed for '"+property+"'; use null or leave out property.")); 102 | } else { 103 | obj[property].forEach(function(item, i) { 104 | validateScalar(obj[property][i], [i, property].concat(path).reverse(), property+"["+i+"]"); 105 | }); 106 | } 107 | } else { 108 | validateScalar(obj[property], [property].concat(path).reverse(), property); 109 | } 110 | }; 111 | 112 | validateCollectionProperty = function(obj, path, property) { 113 | // XXX: Restrict to Collection 114 | validateObjectProperty(obj, path, property); 115 | }; 116 | 117 | validateCollectionPageProperty = function(obj, path, property) { 118 | // XXX: Restrict to CollectionPage or Link 119 | validateObjectProperty(obj, path, property); 120 | }; 121 | 122 | validateContentProperty = function(obj, path, property) { 123 | // XXX: test for other parts of this? 124 | validateStringProperty(obj, path, property); 125 | }; 126 | 127 | validateDateTimeProperty = function(obj, path, property) { 128 | // XXX: test for date format 129 | validateStringProperty(obj, path, property); 130 | }; 131 | 132 | validateLinkProperty = function(obj, path, property) { 133 | // XXX: limit to Link type 134 | validateObjectProperty(obj, path, property); 135 | }; 136 | 137 | validateMediaTypeProperty = function(obj, path, property) { 138 | // XXX: test for media type format 139 | validateStringProperty(obj, path, property); 140 | }; 141 | 142 | validateDurationProperty = function(obj, path, property) { 143 | // XXX: test for media type format 144 | validateStringProperty(obj, path, property); 145 | }; 146 | 147 | validateLanguageProperty = function(obj, path, property) { 148 | // XXX: test for media type format 149 | validateStringProperty(obj, path, property); 150 | }; 151 | 152 | validateMapProperty = function(obj, path, property, base) { 153 | if (_.isUndefined(obj[property]) || _.isNull(obj[property])) { 154 | return; 155 | } else if (!_.isObject(obj[property])) { 156 | notes.push(new Note(Note.ERROR, path, "'"+property+"' MUST be an object mapping language tags to strings.")); 157 | } else { 158 | if (!_.isUndefined(obj[base])) { 159 | notes.push(new Note(Note.ERROR, path, "Define '"+property+"' or '"+base+"' but not both.")); 160 | } 161 | for (var tag in obj[property]) { 162 | if (!_.isString(tag)) { 163 | notes.push(new Note(Note.ERROR, path, "'"+property+"' key must be a string.")); 164 | } 165 | // XXX: test that tag is a language identifier or "und" 166 | if (!_.isString(obj[property][tag])) { 167 | notes.push(new Note(Note.ERROR, path, "'"+property+"["+tag+"]' must be a string.")); 168 | } 169 | } 170 | } 171 | }; 172 | 173 | validateObject = function(obj, path) { 174 | if (!_.isString(obj.name) && !_.isObject(obj.nameMap)) { 175 | notes.push(new Note(Note.WARNING, path, "Object instances SHOULD have a 'name' or 'nameMap' property.")); 176 | } 177 | if (!_.isString(obj.id) && !_.isString(obj['@id'])) { 178 | notes.push(new Note(Note.NOTICE, path, "Object should have an 'id' property.")); 179 | } 180 | validateStringProperty(obj, path, "name"); 181 | validateMapProperty(obj, path, "nameMap", "name"); 182 | validateObjectProperty(obj, path, "attachment"); 183 | validateObjectProperty(obj, path, "attributedTo"); 184 | validateContentProperty(obj, path, "content"); 185 | validateMapProperty(obj, path, "contentMap", "content"); 186 | validateObjectProperty(obj, path, "context"); 187 | validateDateTimeProperty(obj, path, "endTime"); 188 | validateObjectProperty(obj, path, "generator"); 189 | validateObjectProperty(obj, path, "icon"); 190 | validateObjectProperty(obj, path, "image"); 191 | validateObjectProperty(obj, path, "inReplyTo"); 192 | validateObjectProperty(obj, path, "location"); 193 | validateObjectProperty(obj, path, "preview"); 194 | validateDateTimeProperty(obj, path, "published"); 195 | validateCollectionProperty(obj, path, "replies"); 196 | validateObjectProperty(obj, path, "scope"); 197 | validateDateTimeProperty(obj, path, "startTime"); 198 | validateContentProperty(obj, path, "summary"); 199 | validateMapProperty(obj, path, "summaryMap", "summary"); 200 | validateObjectProperty(obj, path, "tag"); 201 | validateDateTimeProperty(obj, path, "updated"); 202 | validateLinkProperty(obj, path, "url"); 203 | validateObjectProperty(obj, path, "to"); 204 | validateObjectProperty(obj, path, "bto"); 205 | validateObjectProperty(obj, path, "cc"); 206 | validateObjectProperty(obj, path, "bcc"); 207 | validateMediaTypeProperty(obj, path, "mediaType"); 208 | validateDurationProperty(obj, path, "duration"); 209 | }; 210 | 211 | validateLink = function(obj, path) { 212 | if (!_.isString(obj.href)) { 213 | notes.push(new Note(Note.ERROR, path, "Link object MUST have an 'href' property.")); 214 | } 215 | validateStringProperty(obj, path, "name"); 216 | validateLanguageProperty(obj, path, "hreflang"); 217 | validateMediaTypeProperty(obj, path, "mediaType"); 218 | validateStringProperty(obj, path, "rel"); 219 | validateNonnegativeIntegerProperty(obj, path, "height"); 220 | validateNonnegativeIntegerProperty(obj, path, "width"); 221 | }; 222 | 223 | validateActivity = function(obj, path) { 224 | validateObject(obj, path); 225 | // XXX: recommendation for actor? 226 | validateObjectProperty(obj, path, "actor"); 227 | validateObjectProperty(obj, path, "object"); 228 | validateObjectProperty(obj, path, "target"); 229 | validateObjectProperty(obj, path, "result"); 230 | validateObjectProperty(obj, path, "origin"); 231 | validateObjectProperty(obj, path, "instrument"); 232 | }; 233 | 234 | validateIntransitiveActivity = function(obj, path) { 235 | validateObject(obj, path); 236 | validateObjectProperty(obj, path, "actor"); 237 | validateObjectProperty(obj, path, "target"); 238 | validateObjectProperty(obj, path, "result"); 239 | validateObjectProperty(obj, path, "origin"); 240 | validateObjectProperty(obj, path, "instrument"); 241 | if ((!_.isUndefined(obj.object) && !_.isNull(obj.object))) 242 | { 243 | notes.push(new Note(Note.WARNING, path, "'object' property is not specified for IntransitiveActivity types.")); 244 | } 245 | }; 246 | 247 | validateActor = function(obj, path) { 248 | validateObject(obj, path); 249 | }; 250 | 251 | validateCollection = function(obj, path) { 252 | validateObject(obj, path); 253 | validateNonnegativeIntegerProperty(obj, path, "totalItems"); 254 | validateCollectionPageProperty(obj, path, "current"); 255 | validateCollectionPageProperty(obj, path, "first"); 256 | validateCollectionPageProperty(obj, path, "last"); 257 | // XXX: prefer array property here? 258 | validateObjectProperty(obj, path, "items"); 259 | }; 260 | 261 | validateOrderedCollection = function(obj, path) { 262 | validateObject(obj, path); 263 | validateNonnegativeIntegerProperty(obj, path, "totalItems"); 264 | validateCollectionPageProperty(obj, path, "current"); 265 | validateCollectionPageProperty(obj, path, "first"); 266 | validateCollectionPageProperty(obj, path, "last"); 267 | // XXX: prefer array property here? 268 | // XXX: check for "items"? 269 | validateObjectProperty(obj, path, "orderedItems"); 270 | }; 271 | 272 | validateCollectionPage = function(obj, path) { 273 | validateCollection(obj, path); 274 | validateCollectionPageProperty(obj, path, "partOf"); 275 | validateCollectionPageProperty(obj, path, "next"); 276 | validateCollectionPageProperty(obj, path, "prev"); 277 | }; 278 | 279 | validateOrderedCollectionPage = function(obj, path) { 280 | validateOrderedCollection(obj, path); 281 | validateNonnegativeIntegerProperty(obj, path, "startIndex"); 282 | validateCollectionPageProperty(obj, path, "partOf"); 283 | validateCollectionPageProperty(obj, path, "next"); 284 | validateCollectionPageProperty(obj, path, "prev"); 285 | }; 286 | 287 | validateQuestion = function(obj, path) { 288 | validateIntransitiveActivity(obj, path); 289 | validateObjectProperty(obj, path, "anyOf"); 290 | validateObjectProperty(obj, path, "oneOf"); 291 | if ((!_.isUndefined(obj.anyOf) && !_.isNull(obj.anyOf)) && 292 | (!_.isUndefined(obj.anyOf) && !_.isNull(obj.oneOf))) 293 | { 294 | notes.push(new Note(Note.ERROR, path, "Question object must not have both 'anyOf' and 'oneOf' properties.")); 295 | } 296 | }; 297 | 298 | validateFunction = function(type) { 299 | var ctype = canonicalType(type); 300 | var typeToFunction = { 301 | "Object": validateObject, 302 | "Link": validateLink, 303 | "Activity": validateActivity, 304 | "IntransitiveActivity": validateIntransitiveActivity, 305 | "Actor": validateActor, 306 | "Collection": validateCollection, 307 | "OrderedCollection": validateOrderedCollection, 308 | "CollectionPage": validateCollectionPage, 309 | "OrderedCollectionPage": validateOrderedCollectionPage, 310 | "Accept": validateActivity, 311 | "Add": validateActivity, 312 | "Announce": validateActivity, 313 | "Arrive": validateIntransitiveActivity, 314 | "Block": validateActivity, 315 | "Create": validateActivity, 316 | "Delete": validateActivity, 317 | "Dislike": validateActivity, 318 | "Flag": validateActivity, 319 | "Follow": validateActivity, 320 | "Ignore": validateActivity, 321 | "Invite": validateActivity, 322 | "Join": validateActivity, 323 | "Leave": validateActivity, 324 | "Like": validateActivity, 325 | "Listen": validateActivity, 326 | "Move": validateActivity, 327 | "Offer": validateActivity, 328 | "Question": validateActivity, 329 | "Reject": validateActivity, 330 | "Read": validateActivity, 331 | "Remove": validateActivity, 332 | "TentativeReject": validateActivity, 333 | "TentativeAccept": validateActivity, 334 | "Travel": validateIntransitiveActivity, 335 | "Undo": validateActivity, 336 | "Update": validateActivity, 337 | "View": validateActivity, 338 | "Application": validateActor, 339 | "Group": validateActor, 340 | "Organization": validateActor, 341 | "Person": validateActor, 342 | "Service": validateActor, 343 | "Article": validateObject, 344 | "Audio": validateObject, 345 | "Document": validateObject, 346 | "Event": validateObject, 347 | "Image": validateObject, 348 | "Note": validateObject, 349 | "Page": validateObject, 350 | "Place": validateObject, 351 | "Profile": validateObject, 352 | "Relationship": validateObject, 353 | "Video": validateObject, 354 | "Mention": validateLink, 355 | "Question": validateQuestion 356 | }; 357 | return typeToFunction[ctype]; 358 | }; 359 | 360 | validateItem = function(obj, path) { 361 | var type = obj.type || obj['@type']; 362 | if (!type) { 363 | notes.push(new Note(Note.NOTICE, path, "Object does not have a type property.")); 364 | } else { 365 | var validateFn = validateFunction(type); 366 | if (!validateFn) { 367 | notes.push(new Note(Note.INFO, path, "Object of unrecognized type " + type)); 368 | } else { 369 | validateFn(obj, path); 370 | } 371 | } 372 | }; 373 | 374 | this.validateTopLevelItem = function(obj) { 375 | var stop = validateJSONType(obj); 376 | if (!stop) { 377 | validateContext(obj); 378 | validateItem(obj, []); 379 | } 380 | }; 381 | 382 | this.validateHTTPResponse = function(url, response) { 383 | var ct = response.headers['content-type']; 384 | if (!ct) { 385 | notes.push(new Note(Note.ERROR, [], "No Content-Type header defined for HTTP response.")); 386 | } else if (ct != 'application/activity+json' && 387 | ct != 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') { 388 | notes.push(new Note(Note.ERROR, [], "Document served with incorrect Content-Type header: " + ct)); 389 | } 390 | }; 391 | 392 | this.validateData = function(data) { 393 | var top = null; 394 | try { 395 | top = JSON.parse(data); 396 | } catch (err) { 397 | notes.push(new Note(Note.ERROR, [], "JSON parsing error: " + err.message)); 398 | } 399 | if (top) { 400 | this.validateTopLevelItem(top); 401 | } 402 | }; 403 | 404 | this.getNotes = function(data) { 405 | return notes.slice(); 406 | }; 407 | }; 408 | 409 | module.exports = Validator; 410 | --------------------------------------------------------------------------------