blob: 84071743b9b14c1fae0e90116453f0a741ee39c4 [file] [log] [blame]
module.exports = function (doc, oldDoc, user, dbCtx) {
function assert (ok, message) {
if (!ok) throw {forbidden:message}
}
// can't write to the db without logging in.
if (!user) {
throw { unauthorized: "Please log in before writing to the db" }
}
try {
require("monkeypatch").patch(Object, Date, Array, String)
} catch (er) {
assert(false, "failed monkeypatching")
}
try {
var semver = require("semver")
var valid = require("valid")
var deep = require("deep")
var deepEquals = deep.deepEquals
} catch (er) {
assert(false, "failed loading modules")
}
try {
if (oldDoc) oldDoc.users = oldDoc.users || {}
doc.users = doc.users || {}
} catch (er) {
assert(false, "failed checking users")
}
// admins can do ANYTHING (even break stuff)
try {
if (isAdmin()) return
} catch (er) {
assert(false, "failed checking admin-ness")
}
// figure out what changed in the doc.
function diffObj (o, n, p) {
p = p || ""
var d = []
var seenKeys = []
for (var i in o) {
seenKeys.push(i)
if (n[i] === undefined) {
d.push("Deleted: "+p+i)
}
else if (typeof o[i] !== typeof n[i]) {
d.push("Changed Type: "+p+i)
}
else if (typeof o[i] === "object") {
if (o[i]) {
if (n[i]) {
d = d.concat(diffObj(o[i], n[i], p + i + "."))
} else {
d.push("Nulled: "+p+i)
}
} else {
if (n[i]) {
d.push("Un-nulled: "+p+i)
} else {
// they're both null, and thus equal. do nothing.
}
}
}
// non-object, non-null
else if (o[i] !== n[i]) {
d.push("Changed: "+p+i+" "+JSON.stringify(o[i]) + " -> "
+JSON.stringify(n[i]))
}
}
for (var i in n) {
if (-1 === seenKeys.indexOf(i)) {
d.push("Added: "+p+i)
}
}
return d
}
// if the doc is an {error:"blerg"}, then throw that right out.
// something detected in the _updates/package script.
// XXX: Make this not ever happen ever. Validation belongs here,
// not in the update function.
try {
assert(!doc.forbidden || doc._deleted, doc.forbidden)
} catch (er) {
assert(false, "failed checking doc.forbidden or doc._deleted")
}
// everyone may alter his "starred" status on any package
try {
if (oldDoc &&
!doc._deleted &&
deepEquals(doc, oldDoc,
[["users", user.name], ["time", "modified"]])) {
return
}
} catch (er) {
assert(false, "failed checking starred stuff")
}
// check if the user is allowed to write to this package.
function validUser () {
if ( !oldDoc || !oldDoc.maintainers ) return true
if (isAdmin()) return true
if (typeof oldDoc.maintainers !== "object") return true
for (var i = 0, l = oldDoc.maintainers.length; i < l; i ++) {
if (oldDoc.maintainers[i].name === user.name) return true
}
return false
}
function isAdmin () {
if (dbCtx &&
dbCtx.admins) {
if (dbCtx.admins.names &&
dbCtx.admins.roles &&
dbCtx.admins.names.indexOf(user.name) !== -1) return true
for (var i=0;i<user.roles.length;i++) {
if (dbCtx.admins.roles.indexOf(user.roles[i]) !== -1) return true
}
}
return user && user.roles.indexOf("_admin") >= 0
}
try {
var vu = validUser()
} catch (er) {
assert(false, "problem checking user validity");
}
if (!vu) {
assert(vu, "user: " + user.name + " not authorized to modify "
+ oldDoc.name + "\n"
+ diffObj(oldDoc, doc).join("\n"))
}
// deleting a document entirely *is* allowed.
if (doc._deleted) return
// sanity checks.
assert(valid.name(doc.name), "name invalid: "+doc.name)
// New documents may only be created with all lowercase names.
// At some point, existing docs will be migrated to lowercase names
// as well.
if (!oldDoc && doc.name !== doc.name.toLowerCase()) {
assert(false, "New packages must have all-lowercase names")
}
assert(doc.name === doc._id, "name must match _id")
assert(!doc.mtime, "doc.mtime is deprecated")
assert(!doc.ctime, "doc.ctime is deprecated")
assert(typeof doc.time === "object", "time must be object")
assert(typeof doc["dist-tags"] === "object", "dist-tags must be object")
var versions = doc.versions
assert(typeof versions === "object", "versions must be object")
var latest = doc["dist-tags"].latest
if (latest) {
assert(versions[latest], "dist-tags.latest must be valid version")
}
// the 'latest' version must have a dist and shasum
// I'd like to also require this of all past versions, but that
// means going back and cleaning up about 2000 old package versions,
// or else *new* versions of those packages can't be published.
// Until that time, do this instead:
var version = versions[latest]
if (version) {
if (!version.dist)
assert(false, "no dist object in " + latest + " version")
if (!version.dist.tarball)
assert(false, "no tarball in " + latest + " version")
if (!version.dist.shasum)
assert(false, "no shasum in " + latest + " version")
}
for (var v in doc["dist-tags"]) {
var ver = doc["dist-tags"][v]
assert(semver.valid(ver, true),
v + " version invalid version: " + ver)
assert(versions[ver],
v + " version missing: " + ver)
}
var depCount = 0
var maxDeps = 5000
function ridiculousDeps() {
if (++depCount > maxDeps)
assert(false, "too many deps. please be less ridiculous.")
}
for (var ver in versions) {
var version = versions[ver]
assert(semver.valid(ver, true),
"invalid version: " + ver)
assert(typeof version === "object",
"version entries must be objects")
assert(version.version === ver,
"version must match: "+ver)
assert(version.name === doc._id,
"version "+ver+" has incorrect name: "+version.name)
depCount = 0
for (var dep in version.dependencies || {}) ridiculousDeps()
for (var dep in version.devDependencies || {}) ridiculousDeps()
for (var dep in version.optionalDependencies || {}) ridiculousDeps()
}
assert(Array.isArray(doc.maintainers),
"maintainers should be a list of owners")
doc.maintainers.forEach(function (m) {
assert(m.name && m.email,
"Maintainer should have name and email: " + JSON.stringify(m))
})
var time = doc.time
var c = new Date(Date.parse(time.created))
, m = new Date(Date.parse(time.modified))
assert(c.toString() !== "Invalid Date",
"invalid created time: " + JSON.stringify(time.created))
assert(m.toString() !== "Invalid Date",
"invalid modified time: " + JSON.stringify(time.modified))
if (oldDoc &&
oldDoc.time &&
oldDoc.time.created &&
Date.parse(oldDoc.time.created)) {
assert(Date.parse(oldDoc.time.created) === Date.parse(time.created),
"created time cannot be changed")
}
if (oldDoc && oldDoc.users) {
assert(deepEquals(doc.users,
oldDoc.users, [[user.name]]),
"you may only alter your own 'star' setting")
}
if (doc.url) {
assert(false,
"Package redirection has been removed. "+
"Please update your publish scripts.")
}
if (doc.description) {
assert(typeof doc.description === 'string',
'"description" field must be a string')
}
// at this point, we've passed the basic sanity tests.
// Time to dig into more details.
// Valid operations:
// 1. Add a version
// 2. Remove a version
// 3. Modify a version
// 4. Add or remove onesself from the "users" hash (already done)
//
// If a version is being added or changed, make sure that the
// _npmUser field matches the current user, and that the
// time object has the proper entry, and that the "maintainers"
// matches the current "maintainers" field.
//
// Things that must not change:
//
// 1. More than one version being modified.
// 2. Removing keys from the "time" hash
//
// Later, once we are off of the update function 3-stage approach,
// these things should also be errors:
//
// 1. Lacking an attachment for any published version.
// 2. Having an attachment for any version not published.
var oldVersions = oldDoc ? oldDoc.versions || {} : {}
var oldTime = oldDoc ? oldDoc.time || {} : {}
var versions = Object.keys(doc.versions)
, modified = null
for (var i = 0, l = versions.length; i < l; i ++) {
var v = versions[i]
if (!v) continue
assert(doc.time[v], "must have time entry for "+v)
if (!deepEquals(doc.versions[v], oldVersions[v], [["directories"], ["deprecated"]]) &&
doc.versions[v]) {
// this one was modified
// if it's more than a few minutes off, then something is wrong.
var t = Date.parse(doc.time[v])
, n = Date.now()
// assert(doc.time[v] !== oldTime[v] &&
// Math.abs(n - t) < 1000 * 60 * 60,
// v + " time needs to be updated\n" +
// "new=" + JSON.stringify(doc.versions[v]) + "\n" +
// "old=" + JSON.stringify(oldVersions[v]))
// var mt = Date.parse(doc.time.modified).getTime()
// , vt = t.getTime()
// assert(Math.abs(mt - vt) < 1000 * 60 * 60,
// v + " is modified, should match modified time")
// XXX Remove the guard these once old docs have been found and
// fixed. It's too big of a pain to have to manually fix
// each one every time someone complains.
if (typeof doc.versions[v]._npmUser !== "object") continue
assert(typeof doc.versions[v]._npmUser === "object",
"_npmUser field must be object\n"+
"(You probably need to upgrade your npm version)")
assert(doc.versions[v]._npmUser.name === user.name,
"_npmUser.name must === user.name")
assert(deepEquals(doc.versions[v].maintainers,
doc.maintainers),
"modified version 'maintainers' must === doc.maintainers")
// make sure that the _npmUser is one of the maintainers
var found = false
for (var j = 0, lm = doc.maintainers.length; j < lm; j ++) {
var m = doc.maintainers[j]
if (m.name === doc.versions[v]._npmUser.name) {
found = true
break
}
}
assert(found, "_npmUser must be a current maintainer.\n"+
"maintainers=" + JSON.stringify(doc.maintainers)+"\n"+
"current user=" + JSON.stringify(doc.versions[v]._npmUser))
} else if (oldTime[v]) {
assert(oldTime[v] === doc.time[v],
v + " time should not be modified 1")
}
}
// now go through all the time settings that weren't covered
for (var v in oldTime) {
if (doc.versions[v] || !oldVersions[v]) continue
assert(doc.time[v] === oldTime[v],
v + " time should not be modified 2")
}
}