From 67bf9b96bbcd92b069fe86d9223fe0f8b9c6e677 Mon Sep 17 00:00:00 2001
From: Chocobozzz <florian.bigard@gmail.com>
Date: Wed, 28 Dec 2016 15:49:23 +0100
Subject: [PATCH] Server: add database field validations

---
 server/helpers/custom-validators/pods.js   |  9 ++-
 server/helpers/custom-validators/videos.js |  6 --
 server/initializers/constants.js           |  2 +-
 server/initializers/installer.js           | 10 ++-
 server/models/application.js               |  8 ++-
 server/models/author.js                    | 13 +++-
 server/models/oauth-client.js              | 11 ++--
 server/models/oauth-token.js               | 19 +++---
 server/models/pod.js                       | 34 ++++++----
 server/models/request.js                   | 14 ++--
 server/models/tag.js                       |  3 +-
 server/models/user.js                      | 32 +++++++---
 server/models/video.js                     | 74 ++++++++++++++++------
 13 files changed, 162 insertions(+), 73 deletions(-)

diff --git a/server/helpers/custom-validators/pods.js b/server/helpers/custom-validators/pods.js
index 0154a2424..8bb3733ff 100644
--- a/server/helpers/custom-validators/pods.js
+++ b/server/helpers/custom-validators/pods.js
@@ -5,14 +5,19 @@ const validator = require('express-validator').validator
 const miscValidators = require('./misc')
 
 const podsValidators = {
-  isEachUniqueHostValid
+  isEachUniqueHostValid,
+  isHostValid
+}
+
+function isHostValid (host) {
+  return validator.isURL(host) && host.split('://').length === 1
 }
 
 function isEachUniqueHostValid (hosts) {
   return miscValidators.isArray(hosts) &&
     hosts.length !== 0 &&
     hosts.every(function (host) {
-      return validator.isURL(host) && host.split('://').length === 1 && hosts.indexOf(host) === hosts.lastIndexOf(host)
+      return isHostValid(host) && hosts.indexOf(host) === hosts.lastIndexOf(host)
     })
 }
 
diff --git a/server/helpers/custom-validators/videos.js b/server/helpers/custom-validators/videos.js
index be8256a80..da857ba5f 100644
--- a/server/helpers/custom-validators/videos.js
+++ b/server/helpers/custom-validators/videos.js
@@ -15,7 +15,6 @@ const videosValidators = {
   isVideoDurationValid,
   isVideoInfoHashValid,
   isVideoNameValid,
-  isVideoPodHostValid,
   isVideoTagsValid,
   isVideoThumbnailValid,
   isVideoThumbnail64Valid
@@ -74,11 +73,6 @@ function isVideoNameValid (value) {
   return validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME)
 }
 
-function isVideoPodHostValid (value) {
-  // TODO: set options (TLD...)
-  return validator.isURL(value)
-}
-
 function isVideoTagsValid (tags) {
   return miscValidators.isArray(tags) &&
          validator.isInt(tags.length, VIDEOS_CONSTRAINTS_FIELDS.TAGS) &&
diff --git a/server/initializers/constants.js b/server/initializers/constants.js
index fc501845a..0af7aca3c 100644
--- a/server/initializers/constants.js
+++ b/server/initializers/constants.js
@@ -69,7 +69,7 @@ const CONSTRAINTS_FIELDS = {
     NAME: { min: 3, max: 50 }, // Length
     DESCRIPTION: { min: 3, max: 250 }, // Length
     EXTNAME: [ '.mp4', '.ogv', '.webm' ],
-    INFO_HASH: { min: 10, max: 50 }, // Length
+    INFO_HASH: { min: 40, max: 40 }, // Length, infohash is 20 bytes length but we represent it in hexa so 20 * 2
     DURATION: { min: 1, max: 7200 }, // Number
     TAGS: { min: 1, max: 3 }, // Number of total tags
     TAG: { min: 2, max: 10 }, // Length
diff --git a/server/initializers/installer.js b/server/initializers/installer.js
index d5382364e..fb63b81ac 100644
--- a/server/initializers/installer.js
+++ b/server/initializers/installer.js
@@ -96,6 +96,7 @@ function createOAuthAdminIfNotExist (callback) {
 
     const username = 'root'
     const role = constants.USER_ROLES.ADMIN
+    const createOptions = {}
     let password = ''
 
     // Do not generate a random password for tests
@@ -105,17 +106,20 @@ function createOAuthAdminIfNotExist (callback) {
       if (process.env.NODE_APP_INSTANCE) {
         password += process.env.NODE_APP_INSTANCE
       }
+
+      // Our password is weak so do not validate it
+      createOptions.validate = false
     } else {
       password = passwordGenerator(8, true)
     }
 
-    const user = db.User.build({
+    const userData = {
       username,
       password,
       role
-    })
+    }
 
-    user.save().asCallback(function (err, createdUser) {
+    db.User.create(userData, createOptions).asCallback(function (err, createdUser) {
       if (err) return callback(err)
 
       logger.info('Username: ' + username)
diff --git a/server/models/application.js b/server/models/application.js
index 4114ed76d..46dcfde33 100644
--- a/server/models/application.js
+++ b/server/models/application.js
@@ -1,9 +1,15 @@
+'use strict'
+
 module.exports = function (sequelize, DataTypes) {
   const Application = sequelize.define('Application',
     {
       migrationVersion: {
         type: DataTypes.INTEGER,
-        defaultValue: 0
+        defaultValue: 0,
+        allowNull: false,
+        validate: {
+          isInt: true
+        }
       }
     },
     {
diff --git a/server/models/author.js b/server/models/author.js
index 493c2ca11..e0ac868ea 100644
--- a/server/models/author.js
+++ b/server/models/author.js
@@ -1,8 +1,19 @@
+'use strict'
+
+const customUsersValidators = require('../helpers/custom-validators').users
+
 module.exports = function (sequelize, DataTypes) {
   const Author = sequelize.define('Author',
     {
       name: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          usernameValid: function (value) {
+            const res = customUsersValidators.isUserUsernameValid(value)
+            if (res === false) throw new Error('Username is not valid.')
+          }
+        }
       }
     },
     {
diff --git a/server/models/oauth-client.js b/server/models/oauth-client.js
index 15118591a..b56838d4c 100644
--- a/server/models/oauth-client.js
+++ b/server/models/oauth-client.js
@@ -1,11 +1,15 @@
+'use strict'
+
 module.exports = function (sequelize, DataTypes) {
   const OAuthClient = sequelize.define('OAuthClient',
     {
       clientId: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false
       },
       clientSecret: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false
       },
       grants: {
         type: DataTypes.ARRAY(DataTypes.STRING)
@@ -28,9 +32,6 @@ module.exports = function (sequelize, DataTypes) {
   return OAuthClient
 }
 
-// TODO: validation
-// OAuthClientSchema.path('clientSecret').required(true)
-
 // ---------------------------------------------------------------------------
 
 function associate (models) {
diff --git a/server/models/oauth-token.js b/server/models/oauth-token.js
index c9108bf95..f8de4e916 100644
--- a/server/models/oauth-token.js
+++ b/server/models/oauth-token.js
@@ -1,3 +1,5 @@
+'use strict'
+
 const logger = require('../helpers/logger')
 
 // ---------------------------------------------------------------------------
@@ -6,16 +8,20 @@ module.exports = function (sequelize, DataTypes) {
   const OAuthToken = sequelize.define('OAuthToken',
     {
       accessToken: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false
       },
       accessTokenExpiresAt: {
-        type: DataTypes.DATE
+        type: DataTypes.DATE,
+        allowNull: false
       },
       refreshToken: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false
       },
       refreshTokenExpiresAt: {
-        type: DataTypes.DATE
+        type: DataTypes.DATE,
+        allowNull: false
       }
     },
     {
@@ -33,11 +39,6 @@ module.exports = function (sequelize, DataTypes) {
   return OAuthToken
 }
 
-// TODO: validation
-// OAuthTokenSchema.path('accessToken').required(true)
-// OAuthTokenSchema.path('client').required(true)
-// OAuthTokenSchema.path('user').required(true)
-
 // ---------------------------------------------------------------------------
 
 function associate (models) {
diff --git a/server/models/pod.js b/server/models/pod.js
index fff6970a7..84f78f200 100644
--- a/server/models/pod.js
+++ b/server/models/pod.js
@@ -3,6 +3,7 @@
 const map = require('lodash/map')
 
 const constants = require('../initializers/constants')
+const customPodsValidators = require('../helpers/custom-validators').pods
 
 // ---------------------------------------------------------------------------
 
@@ -10,14 +11,27 @@ module.exports = function (sequelize, DataTypes) {
   const Pod = sequelize.define('Pod',
     {
       host: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          isHost: function (value) {
+            const res = customPodsValidators.isHostValid(value)
+            if (res === false) throw new Error('Host not valid.')
+          }
+        }
       },
       publicKey: {
-        type: DataTypes.STRING(5000)
+        type: DataTypes.STRING(5000),
+        allowNull: false
       },
       score: {
         type: DataTypes.INTEGER,
-        defaultValue: constants.FRIEND_SCORE.BASE
+        defaultValue: constants.FRIEND_SCORE.BASE,
+        allowNull: false,
+        validate: {
+          isInt: true,
+          max: constants.FRIEND_SCORE.MAX
+        }
       }
     },
     {
@@ -42,12 +56,6 @@ module.exports = function (sequelize, DataTypes) {
   return Pod
 }
 
-// TODO: max score -> constants.FRIENDS_SCORE.MAX
-// TODO: validation
-// PodSchema.path('host').validate(validator.isURL)
-// PodSchema.path('publicKey').required(true)
-// PodSchema.path('score').validate(function (value) { return !isNaN(value) })
-
 // ------------------------------ METHODS ------------------------------
 
 function toFormatedJSON () {
@@ -82,15 +90,17 @@ function incrementScores (ids, value, callback) {
     score: this.sequelize.literal('score +' + value)
   }
 
-  const query = {
+  const options = {
     where: {
       id: {
         $in: ids
       }
-    }
+    },
+    // In this case score is a literal and not an integer so we do not validate it
+    validate: false
   }
 
-  return this.update(update, query).asCallback(callback)
+  return this.update(update, options).asCallback(callback)
 }
 
 function list (callback) {
diff --git a/server/models/request.js b/server/models/request.js
index 70aa32610..e18f8fe3d 100644
--- a/server/models/request.js
+++ b/server/models/request.js
@@ -3,6 +3,7 @@
 const each = require('async/each')
 const eachLimit = require('async/eachLimit')
 const waterfall = require('async/waterfall')
+const values = require('lodash/values')
 
 const constants = require('../initializers/constants')
 const logger = require('../helpers/logger')
@@ -17,11 +18,12 @@ module.exports = function (sequelize, DataTypes) {
   const Request = sequelize.define('Request',
     {
       request: {
-        type: DataTypes.JSON
+        type: DataTypes.JSON,
+        allowNull: false
       },
       endpoint: {
-        // TODO: enum?
-        type: DataTypes.STRING
+        type: DataTypes.ENUM(values(constants.REQUEST_ENDPOINTS)),
+        allowNull: false
       }
     },
     {
@@ -196,7 +198,7 @@ function makeRequests () {
 
         makeRequest(toPod, requestToMake.endpoint, requestToMake.datas, function (success) {
           if (success === true) {
-            logger.debug('Removing requests for %s pod.', requestToMake.toPodId, { requestsIds: requestToMake.ids })
+            logger.debug('Removing requests for pod %s.', requestToMake.toPodId, { requestsIds: requestToMake.ids })
 
             goodPods.push(requestToMake.toPodId)
 
@@ -261,13 +263,13 @@ function updatePodsScore (goodPods, badPods) {
 
   if (goodPods.length !== 0) {
     Pod.incrementScores(goodPods, constants.PODS_SCORE.BONUS, function (err) {
-      if (err) logger.error('Cannot increment scores of good pods.')
+      if (err) logger.error('Cannot increment scores of good pods.', { error: err })
     })
   }
 
   if (badPods.length !== 0) {
     Pod.incrementScores(badPods, constants.PODS_SCORE.MALUS, function (err) {
-      if (err) logger.error('Cannot decrement scores of bad pods.')
+      if (err) logger.error('Cannot decrement scores of bad pods.', { error: err })
       removeBadPods.call(self)
     })
   }
diff --git a/server/models/tag.js b/server/models/tag.js
index 874e88842..d6c2d3bb1 100644
--- a/server/models/tag.js
+++ b/server/models/tag.js
@@ -6,7 +6,8 @@ module.exports = function (sequelize, DataTypes) {
   const Tag = sequelize.define('Tag',
     {
       name: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false
       }
     },
     {
diff --git a/server/models/user.js b/server/models/user.js
index e50eb96ea..944986a44 100644
--- a/server/models/user.js
+++ b/server/models/user.js
@@ -1,5 +1,11 @@
+'use strict'
+
+const values = require('lodash/values')
+
 const modelUtils = require('./utils')
+const constants = require('../initializers/constants')
 const peertubeCrypto = require('../helpers/peertube-crypto')
+const customUsersValidators = require('../helpers/custom-validators').users
 
 // ---------------------------------------------------------------------------
 
@@ -7,13 +13,28 @@ module.exports = function (sequelize, DataTypes) {
   const User = sequelize.define('User',
     {
       password: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          passwordValid: function (value) {
+            const res = customUsersValidators.isUserPasswordValid(value)
+            if (res === false) throw new Error('Password not valid.')
+          }
+        }
       },
       username: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          usernameValid: function (value) {
+            const res = customUsersValidators.isUserUsernameValid(value)
+            if (res === false) throw new Error('Username not valid.')
+          }
+        }
       },
       role: {
-        type: DataTypes.STRING
+        type: DataTypes.ENUM(values(constants.USER_ROLES)),
+        allowNull: false
       }
     },
     {
@@ -41,11 +62,6 @@ module.exports = function (sequelize, DataTypes) {
   return User
 }
 
-// TODO: Validation
-// UserSchema.path('password').required(customUsersValidators.isUserPasswordValid)
-// UserSchema.path('username').required(customUsersValidators.isUserUsernameValid)
-// UserSchema.path('role').validate(customUsersValidators.isUserRoleValid)
-
 function beforeCreateOrUpdate (user, options, next) {
   peertubeCrypto.cryptPassword(user.password, function (err, hash) {
     if (err) return next(err)
diff --git a/server/models/video.js b/server/models/video.js
index 04478c8d7..3ebc48ad4 100644
--- a/server/models/video.js
+++ b/server/models/video.js
@@ -8,10 +8,12 @@ const map = require('lodash/map')
 const parallel = require('async/parallel')
 const parseTorrent = require('parse-torrent')
 const pathUtils = require('path')
+const values = require('lodash/values')
 
 const constants = require('../initializers/constants')
 const logger = require('../helpers/logger')
 const modelUtils = require('./utils')
+const customVideosValidators = require('../helpers/custom-validators').videos
 
 // ---------------------------------------------------------------------------
 
@@ -22,26 +24,61 @@ module.exports = function (sequelize, DataTypes) {
       id: {
         type: DataTypes.UUID,
         defaultValue: DataTypes.UUIDV4,
-        primaryKey: true
+        primaryKey: true,
+        validate: {
+          isUUID: 4
+        }
       },
       name: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          nameValid: function (value) {
+            const res = customVideosValidators.isVideoNameValid(value)
+            if (res === false) throw new Error('Video name is not valid.')
+          }
+        }
       },
       extname: {
-        // TODO: enum?
-        type: DataTypes.STRING
+        type: DataTypes.ENUM(values(constants.CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)),
+        allowNull: false
       },
       remoteId: {
-        type: DataTypes.UUID
+        type: DataTypes.UUID,
+        allowNull: true,
+        validate: {
+          isUUID: 4
+        }
       },
       description: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          descriptionValid: function (value) {
+            const res = customVideosValidators.isVideoDescriptionValid(value)
+            if (res === false) throw new Error('Video description is not valid.')
+          }
+        }
       },
       infoHash: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          infoHashValid: function (value) {
+            const res = customVideosValidators.isVideoInfoHashValid(value)
+            if (res === false) throw new Error('Video info hash is not valid.')
+          }
+        }
       },
       duration: {
-        type: DataTypes.INTEGER
+        type: DataTypes.INTEGER,
+        allowNull: false,
+        validate: {
+          durationValid: function (value) {
+            const res = customVideosValidators.isVideoDurationValid(value)
+            if (res === false) throw new Error('Video duration is not valid.')
+          }
+        }
       }
     },
     {
@@ -71,6 +108,7 @@ module.exports = function (sequelize, DataTypes) {
         toRemoteJSON
       },
       hooks: {
+        beforeValidate,
         beforeCreate,
         afterDestroy
       }
@@ -80,13 +118,14 @@ module.exports = function (sequelize, DataTypes) {
   return Video
 }
 
-// TODO: Validation
-// VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
-// VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
-// VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid)
-// VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
-// VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
-// VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
+function beforeValidate (video, options, next) {
+  if (video.isOwned()) {
+    // 40 hexa length
+    video.infoHash = '0123456789abcdef0123456789abcdef01234567'
+  }
+
+  return next(null)
+}
 
 function beforeCreate (video, options, next) {
   const tasks = []
@@ -113,9 +152,8 @@ function beforeCreate (video, options, next) {
             if (err) return callback(err)
 
             const parsedTorrent = parseTorrent(torrent)
-            video.infoHash = parsedTorrent.infoHash
-
-            callback(null)
+            video.set('infoHash', parsedTorrent.infoHash)
+            video.validate().asCallback(callback)
           })
         })
       },