diff --git a/app.js b/app.js index 5619f37e81..01e76fcd4c 100644 --- a/app.js +++ b/app.js @@ -324,7 +324,7 @@ app.get( ); app.get( - '/stories/submit/:newStory', + '/stories/submit/new-story', storyController.preSubmit ); diff --git a/controllers/resources.js b/controllers/resources.js index fd69fc83e1..617f5492ba 100644 --- a/controllers/resources.js +++ b/controllers/resources.js @@ -4,7 +4,6 @@ var User = require('../models/User'), Story = require('./../models/Story'), Comment = require('./../models/Comment'), resources = require('./resources.json'), - questions = resources.questions, steps = resources.steps, secrets = require('./../config/secrets'), bonfires = require('../seed_data/bonfires.json'), @@ -154,7 +153,7 @@ module.exports = { }, bloggerCalls: function(req, res) { request('https://www.googleapis.com/blogger/v3/blogs/2421288658305323950/posts?key=' + secrets.blogger.key, function (err, status, blog) { - var blog = blog.length > 100 ? JSON.parse(blog) : ""; + blog = blog.length > 100 ? JSON.parse(blog) : ''; res.send({ blog1Title: blog ? blog["items"][0]["title"] : "Can't connect to Blogger", blog1Link: blog ? blog["items"][0]["url"] : "http://blog.freecodecamp.com", @@ -225,10 +224,6 @@ module.exports = { return compliments[Math.floor(Math.random() * compliments.length)]; }, - numberOfBonfires: function() { - return bonfires.length - 1; - }, - allBonfireIds: function() { return bonfires.map(function(elem) { return { @@ -290,14 +285,19 @@ module.exports = { return process.env.NODE_ENV; }, getURLTitle: function(url, callback) { - + debug('got url in meta scraping function', url); (function () { - var result = {title: ''}; + var result = {title: '', image: '', url: '', description: ''}; request(url, function (error, response, body) { if (!error && response.statusCode === 200) { var $ = cheerio.load(body); - var title = $('title').text(); - result.title = title; + var metaDescription = $("meta[name='description']"); + var metaImage = $("meta[property='og:image']"); + var urlImage = metaImage.attr('content') ? metaImage.attr('content') : ''; + var description = metaDescription.attr('content') ? metaDescription.attr('content') : ''; + result.title = $('title').text(); + result.image = urlImage; + result.description = description; callback(null, result); } else { callback('failed'); diff --git a/controllers/story.js b/controllers/story.js index 78ceda95d4..e5d1788e0a 100644 --- a/controllers/story.js +++ b/controllers/story.js @@ -8,9 +8,9 @@ var R = require('ramda'), mongodb = require('mongodb'), MongoClient = mongodb.MongoClient, secrets = require('../config/secrets'), - User = require('./../models/User'); + sanitizeHtml = require('sanitize-html'); -function hotRank(timeValue, rank, headline) { +function hotRank(timeValue, rank) { /* * Hotness ranking algorithm: http://amix.dk/blog/post/19588 * tMS = postedOnDate - foundationTime; @@ -24,21 +24,20 @@ function hotRank(timeValue, rank, headline) { } -exports.hotJSON = function(req, res, next) { +exports.hotJSON = function(req, res) { var story = Story.find({}).sort({'timePosted': -1}).limit(1000); story.exec(function(err, stories) { if (err) { - throw err; + res.send(500); + return next(err); } var foundationDate = 1413298800000; var sliceVal = stories.length >= 100 ? 100 : stories.length; - var rankedStories = stories; - return res.json(rankedStories.map(function(elem) { + return res.json(stories.map(function(elem) { return elem; }).sort(function(a, b) { - debug('a rank and b rank', hotRank(a.timePosted - foundationDate, a.rank, a.headline), hotRank(b.timePosted - foundationDate, b.rank, b.headline)); return hotRank(b.timePosted - foundationDate, b.rank, b.headline) - hotRank(a.timePosted - foundationDate, a.rank, a.headline); }).slice(0, sliceVal)); @@ -49,48 +48,61 @@ exports.recentJSON = function(req, res, next) { var story = Story.find({}).sort({'timePosted': -1}).limit(100); story.exec(function(err, stories) { if (err) { - throw err; + res.status(500); + return next(err); } res.json(stories); }); }; -exports.hot = function(req, res, next) { +exports.hot = function(req, res) { res.render('stories/index', { page: 'hot' }); }; -exports.submitNew = function(req,res, next) { +exports.submitNew = function(req, res) { res.render('stories/index', { page: 'submit' }); }; -exports.search = function(req, res, next) { +exports.search = function(req, res) { res.render('stories/index', { page: 'search' }); }; -exports.recent = function(req, res, next) { +exports.recent = function(req, res) { res.render('stories/index', { page: 'recent' }); }; -exports.preSubmit = function(req, res, next) { +exports.preSubmit = function(req, res) { - var data = req.params.newStory; + var data = req.query; + var cleanData = sanitizeHtml(data.url); + if (data.url.replace(/&/g, '&') !== cleanData) { + debug('data and cleandata', data, cleanData, data.url === cleanData); + req.flash('errors', { + msg: 'The data for this post is malformed' + }); + return res.render('stories/index', { + page: 'stories/submit' + }); + } - data = data.replace(/url=/gi, '').replace(/&title=/gi, ',').split(','); - var url = data[0]; - var title = data[1]; - res.render('stories/index', { + var title = data.title || ''; + var image = data.image || ''; + var description = data.description || ''; + return res.render('stories/index', { page: 'storySubmission', - storyURL: url, - storyTitle: title + storyURL: data.url, + storyTitle: title, + storyImage: image, + storyMetaDescription: description }); }; @@ -132,15 +144,15 @@ exports.returnIndividualStory = function(req, res, next) { user: req.user, timeAgo: moment(story.timePosted).fromNow(), image: story.image, - page: 'show' + page: 'show', + storyMetaDescription: story.metaDescription }); }); }; -exports.getStories = function(req, res, next) { +exports.getStories = function(req, res) { MongoClient.connect(secrets.db, function(err, database) { - var db = database; - db.collection('stories').find({ + database.collection('stories').find({ "$text": { "$search": req.body.data.searchValue } @@ -155,6 +167,7 @@ exports.getStories = function(req, res, next) { comments: 1, image: 1, storyLink: 1, + metaDescription: 1, textScore: { $meta: "textScore" } @@ -177,7 +190,8 @@ exports.upvote = function(req, res, next) { var data = req.body.data; Story.find({'_id': data.id}, function(err, story) { if (err) { - throw err; + res.status(500); + return next(err); } story = story.pop(); story.rank++; @@ -197,59 +211,68 @@ exports.comments = function(req, res, next) { var data = req.params.id; Comment.find({'_id': data}, function(err, comment) { if (err) { - throw err; + res.status(500); + return next(err); } comment = comment.pop(); return res.send(comment); }); }; -exports.newStory = function(req, res, next) { +exports.newStory = function(req, res) { var url = req.body.data.url; + var cleanURL = sanitizeHtml(url); + if (cleanURL !== url) { + req.flash('errors', { + msg: "The URL you submitted doesn't appear valid" + }); + return res.json({ + alreadyPosted: true, + storyURL: '/stories/submit' + }); + + } if (url.search(/^https?:\/\//g) === -1) { url = 'http://' + url; } - debug('In pre submit with a url', url); - Story.find({'link': url}, function(err, story) { - debug('Attempting to find a story'); if (err) { - debug('oops'); return res.status(500); } if (story.length) { - debug('Found a story already, here\'s the return from find', story); req.flash('errors', { msg: "Someone's already posted that link. Here's the discussion." }); - debug('Redirecting the user with', story[0].storyLink); return res.json({ alreadyPosted: true, - storyURL: story.pop().storyLink + storyURL: '/stories/' + story.pop().storyLink }); } resources.getURLTitle(url, processResponse); }); - function processResponse(err, storyTitle) { + function processResponse(err, story) { if (err) { res.json({ alreadyPosted: false, storyURL: url, - storyTitle: '' + storyTitle: '', + storyImage: '', + storyMetaDescription: '' }); } else { - storyTitle = storyTitle ? storyTitle : ''; res.json({ alreadyPosted: false, storyURL: url, - storyTitle: storyTitle.title + storyTitle: story.title, + storyImage: story.image, + storyMetaDescription: story.description }); } } }; -exports.storySubmission = function(req, res, next) { +exports.storySubmission = function(req, res) { var data = req.body.data; var storyLink = data.headline .replace(/\'/g, '') @@ -263,19 +286,20 @@ exports.storySubmission = function(req, res, next) { link = 'http://' + link; } var story = new Story({ - headline: data.headline, + headline: sanitizeHtml(data.headline), timePosted: Date.now(), link: link, - description: data.description, + description: sanitizeHtml(data.description), rank: 1, upVotes: data.upVotes, author: data.author, comments: [], image: data.image, - storyLink: storyLink + storyLink: storyLink, + metaDescription: data.storyMetaDescription }); - story.save(function(err, data) { + story.save(function(err) { if (err) { return res.status(500); } @@ -285,12 +309,22 @@ exports.storySubmission = function(req, res, next) { }); }; -exports.commentSubmit = function(req, res, next) { - debug('comment submit fired'); +exports.commentSubmit = function(req, res) { var data = req.body.data; + var sanitizedBody = sanitizeHtml(data.body, + { + allowedTags: [], + allowedAttributes: [] + }); + if (data.body !== sanitizedBody) { + req.flash('errors', { + msg: 'HTML is not allowed' + }); + return res.send(true); + } var comment = new Comment({ associatedPost: data.associatedPost, - body: data.body, + body: sanitizedBody, rank: 0, upvotes: 0, author: data.author, @@ -301,13 +335,22 @@ exports.commentSubmit = function(req, res, next) { commentSave(comment, Story, res); }; -exports.commentOnCommentSubmit = function(req, res, next) { - debug('comment on comment submit'); - var idToFind = req.params.id; +exports.commentOnCommentSubmit = function(req, res) { var data = req.body.data; + var sanitizedBody = sanitizeHtml(data.body, + { + allowedTags: [], + allowedAttributes: [] + }); + if (data.body !== sanitizedBody) { + req.flash('errors', { + msg: 'HTML is not allowed' + }); + return res.send(true); + } var comment = new Comment({ associatedPost: data.associatedPost, - body: data.body, + body: sanitizedBody, rank: 0, upvotes: 0, author: data.author, @@ -331,7 +374,7 @@ function commentSave(comment, Context, res) { associatedStory = associatedStory.pop(); if (associatedStory) { associatedStory.comments.push(data._id); - associatedStory.save(function (err, data) { + associatedStory.save(function (err) { if (err) { res.status(500); } diff --git a/controllers/user.js b/controllers/user.js index d7e383a251..07a0523d5f 100644 --- a/controllers/user.js +++ b/controllers/user.js @@ -7,10 +7,10 @@ var _ = require('lodash'), secrets = require('../config/secrets'), moment = require('moment'), Challenge = require('./../models/Challenge'), - debug = require('debug')('freecc:cntr:challenges') + debug = require('debug')('freecc:cntr:challenges'), resources = require('./resources'); -//TODO(Berks): Refactor to use module.exports = {} pattern. + /** * GET /signin @@ -18,10 +18,10 @@ var _ = require('lodash'), */ exports.getSignin = function(req, res) { - if (req.user) return res.redirect('/'); - res.render('account/signin', { - title: 'Free Code Camp Login' - }); + if (req.user) return res.redirect('/'); + res.render('account/signin', { + title: 'Free Code Camp Login' + }); }; /** @@ -30,28 +30,28 @@ exports.getSignin = function(req, res) { */ exports.postSignin = function(req, res, next) { - req.assert('email', 'Email is not valid').isEmail(); - req.assert('password', 'Password cannot be blank').notEmpty(); + req.assert('email', 'Email is not valid').isEmail(); + req.assert('password', 'Password cannot be blank').notEmpty(); - var errors = req.validationErrors(); + var errors = req.validationErrors(); - if (errors) { - req.flash('errors', errors); - return res.redirect('/signin'); - } - - passport.authenticate('local', function(err, user, info) { - if (err) return next(err); - if (!user) { - req.flash('errors', { msg: info.message }); - return res.redirect('/signin'); + if (errors) { + req.flash('errors', errors); + return res.redirect('/signin'); } - req.logIn(user, function(err) { - if (err) return next(err); - req.flash('success', { msg: 'Success! You are logged in.' }); - res.redirect(req.session.returnTo || '/'); - }); - })(req, res, next); + + passport.authenticate('local', function(err, user, info) { + if (err) return next(err); + if (!user) { + req.flash('errors', { msg: info.message }); + return res.redirect('/signin'); + } + req.logIn(user, function(err) { + if (err) return next(err); + req.flash('success', { msg: 'Success! You are logged in.' }); + res.redirect(req.session.returnTo || '/'); + }); + })(req, res, next); }; /** @@ -60,8 +60,8 @@ exports.postSignin = function(req, res, next) { */ exports.signout = function(req, res) { - req.logout(); - res.redirect('/'); + req.logout(); + res.redirect('/'); }; /** @@ -70,10 +70,10 @@ exports.signout = function(req, res) { */ exports.getEmailSignin = function(req, res) { - if (req.user) return res.redirect('/'); - res.render('account/email-signin', { - title: 'Sign in to your Free Code Camp Account' - }); + if (req.user) return res.redirect('/'); + res.render('account/email-signin', { + title: 'Sign in to your Free Code Camp Account' + }); }; /** @@ -82,10 +82,10 @@ exports.getEmailSignin = function(req, res) { */ exports.getEmailSignup = function(req, res) { - if (req.user) return res.redirect('/'); - res.render('account/email-signup', { - title: 'Create Your Free Code Camp Account' - }); + if (req.user) return res.redirect('/'); + res.render('account/email-signup', { + title: 'Create Your Free Code Camp Account' + }); }; /** @@ -94,64 +94,93 @@ exports.getEmailSignup = function(req, res) { */ exports.postEmailSignup = function(req, res, next) { - var errors = req.validationErrors(); + var errors = req.validationErrors(); - if (errors) { - req.flash('errors', errors); - return res.redirect('/email-signup'); - debug(errors); - } - - var user = new User({ - email: req.body.email.trim(), - password: req.body.password, - profile : { - username: req.body.username.trim(), - picture: 'https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-180x180.png' + if (errors) { + req.flash('errors', errors); + return res.redirect('/email-signup'); } - }); - User.findOne({ email: req.body.email }, function(err, existingUser) { - if (err) { return next(err); } + var possibleUserData = req.body; - if (existingUser) { - req.flash('errors', { - msg: 'Account with that email address already exists.' - }); - return res.redirect('/email-signup'); + if (possibleUserData.password.length < 8) { + req.flash('errors', { + msg: 'Your password is too short' + }); + return res.redirect('email-signup'); } - user.save(function(err) { - if (err) { return next(err); } - req.logIn(user, function(err) { - if (err) { return next(err); } - res.redirect('/email-signup'); - }); + if (possibleUserData.username.length < 5 || possibleUserData.length > 20) { + req.flash('errors', { + msg: 'Your username must be between 5 and 20 characters' + }); + return res.redirect('email-signup'); + } + + + var user = new User({ + email: req.body.email.trim(), + password: req.body.password, + profile : { + username: req.body.username.trim(), + picture: 'https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-180x180.png' + } }); - var transporter = nodemailer.createTransport({ - service: 'Mandrill', - auth: { - user: secrets.mandrill.user, - pass: secrets.mandrill.password - } + + User.findOne({ email: req.body.email }, function(err, existingEmail) { + if (err) { + return next(err); + } + + if (existingEmail) { + req.flash('errors', { + msg: 'Account with that email address already exists.' + }); + return res.redirect('/email-signup'); + } + User.findOne({'profile.username': req.body.username }, function(err, existingUsername) { + if (err) { + return next(err); + } + if (existingUsername) { + req.flash('errors', { + msg: 'Account with that username already exists.' + }); + return res.redirect('/email-signup'); + } + + user.save(function(err) { + if (err) { return next(err); } + req.logIn(user, function(err) { + if (err) { return next(err); } + res.redirect('/email-signup'); + }); + }); + var transporter = nodemailer.createTransport({ + service: 'Mandrill', + auth: { + user: secrets.mandrill.user, + pass: secrets.mandrill.password + } + }); + var mailOptions = { + to: user.email, + from: 'Team@freecodecamp.com', + subject: 'Welcome to Free Code Camp!', + text: [ + 'Greetings from San Francisco!\n\n', + 'Thank you for joining our community.\n', + 'Feel free to email us at this address if you have any questions about Free Code Camp.\n', + "And if you have a moment, check out our blog: blog.freecodecamp.com.\n", + 'Good luck with the challenges!\n\n', + '- the Volunteer Camp Counselor Team' + ].join('') + }; + transporter.sendMail(mailOptions, function(err) { + if (err) { return err; } + }); + }); }); - var mailOptions = { - to: user.email, - from: 'Team@freecodecamp.com', - subject: 'Welcome to Free Code Camp!', - text: [ - 'Greetings from San Francisco!\n\n', - 'Thank you for joining our community.\n', - 'Feel free to email us at this address if you have any questions about Free Code Camp.\n', - "And if you have a moment, check out our blog: blog.freecodecamp.com.\n", - 'Good luck with the challenges!\n\n', - '- the Volunteer Camp Counselor Team' - ].join('') - }; - transporter.sendMail(mailOptions, function(err) { - if (err) { return err; } - }); - }); }; /** @@ -161,7 +190,7 @@ exports.postEmailSignup = function(req, res, next) { exports.getAccount = function(req, res) { res.render('account/account', { - title: 'Manage your Free Code Camp Account' + title: 'Manage your Free Code Camp Account' }); }; @@ -169,9 +198,9 @@ exports.getAccount = function(req, res) { * Angular API Call */ - exports.getAccountAngular = function(req, res) { +exports.getAccountAngular = function(req, res) { res.json({ - user: req.user + user: req.user }); }; @@ -180,13 +209,13 @@ exports.getAccount = function(req, res) { */ exports.checkUniqueUsername = function(req, res) { - User.count({'profile.username': req.params.username.toLowerCase()}, function (err, data) { - if (data == 1) { - return res.send(true); - } else { - return res.send(false); - } - }); + User.count({'profile.username': req.params.username.toLowerCase()}, function (err, data) { + if (data == 1) { + return res.send(true); + } else { + return res.send(false); + } + }); }; /** @@ -207,13 +236,13 @@ exports.checkExistingUsername = function(req, res) { */ exports.checkUniqueEmail = function(req, res) { - User.count({'email': decodeURIComponent(req.params.email).toLowerCase()}, function (err, data) { - if (data == 1) { - return res.send(true); - } else { - return res.send(false); - } - }); + User.count({'email': decodeURIComponent(req.params.email).toLowerCase()}, function (err, data) { + if (data == 1) { + return res.send(true); + } else { + return res.send(false); + } + }); }; @@ -223,44 +252,44 @@ exports.checkUniqueEmail = function(req, res) { */ exports.returnUser = function(req, res, next) { - User.find({'profile.username': req.params.username.toLowerCase()}, function(err, user) { - if (err) { debug('Username err: ', err); next(err); } - if (user[0]) { - var user = user[0]; - Challenge.find({}, null, {sort: {challengeNumber: 1}}, function (err, c) { - res.render('account/show', { - title: 'Camper: ', - username: user.profile.username, - name: user.profile.name, - location: user.profile.location, - githubProfile: user.profile.githubProfile, - linkedinProfile: user.profile.linkedinProfile, - codepenProfile: user.profile.codepenProfile, - twitterHandle: user.profile.twitterHandle, - bio: user.profile.bio, - picture: user.profile.picture, - points: user.points, - website1Link: user.portfolio.website1Link, - website1Title: user.portfolio.website1Title, - website1Image: user.portfolio.website1Image, - website2Link: user.portfolio.website2Link, - website2Title: user.portfolio.website2Title, - website2Image: user.portfolio.website2Image, - website3Link: user.portfolio.website3Link, - website3Title: user.portfolio.website3Title, - website3Image: user.portfolio.website3Image, - challenges: c, - ch: user.challengesHash, - moment: moment - }); - }); - } else { - req.flash('errors', { - msg: "404: We couldn't find a page with that url. Please double check the link." - }); - return res.redirect('/'); - } - }); + User.find({'profile.username': req.params.username.toLowerCase()}, function(err, user) { + if (err) { debug('Username err: ', err); next(err); } + if (user[0]) { + var user = user[0]; + Challenge.find({}, null, {sort: {challengeNumber: 1}}, function (err, c) { + res.render('account/show', { + title: 'Camper: ', + username: user.profile.username, + name: user.profile.name, + location: user.profile.location, + githubProfile: user.profile.githubProfile, + linkedinProfile: user.profile.linkedinProfile, + codepenProfile: user.profile.codepenProfile, + twitterHandle: user.profile.twitterHandle, + bio: user.profile.bio, + picture: user.profile.picture, + points: user.points, + website1Link: user.portfolio.website1Link, + website1Title: user.portfolio.website1Title, + website1Image: user.portfolio.website1Image, + website2Link: user.portfolio.website2Link, + website2Title: user.portfolio.website2Title, + website2Image: user.portfolio.website2Image, + website3Link: user.portfolio.website3Link, + website3Title: user.portfolio.website3Title, + website3Image: user.portfolio.website3Image, + challenges: c, + ch: user.challengesHash, + moment: moment + }); + }); + } else { + req.flash('errors', { + msg: "404: We couldn't find a page with that url. Please double check the link." + }); + return res.redirect('/'); + } + }); }; @@ -292,69 +321,70 @@ exports.updateProgress = function(req, res) { */ exports.postUpdateProfile = function(req, res, next) { - User.findById(req.user.id, function(err, user) { - if (err) return next(err); - var errors = req.validationErrors(); - if (errors) { - req.flash('errors', errors); - return res.redirect('/account'); - } - User.findOne({ email: req.body.email }, function(err, existingEmail) { - if (err) { - return next(err); - } - var user = req.user; - if (existingEmail && existingEmail.email != user.email) { - req.flash('errors', { - msg: "An account with that email address already exists." - }); - return res.redirect('/account'); - } - User.findOne({ username: req.body.username }, function(err, existingUsername) { - if (err) { - return next(err); + // What does this do? + User.findById(req.user.id, function(err, user) { + if (err) return next(err); + var errors = req.validationErrors(); + if (errors) { + req.flash('errors', errors); + return res.redirect('/account'); } - var user = req.user; - if (existingUsername && existingUsername.profile.username !== user.profile.username) { - req.flash('errors', { - msg: 'An account with that username already exists.' - }); - return res.redirect('/account'); - } - var user = req.user; - user.email = req.body.email.trim() || ''; - user.profile.name = req.body.name.trim() || ''; - user.profile.username = req.body.username.trim() || ''; - user.profile.location = req.body.location.trim() || ''; - user.profile.githubProfile = req.body.githubProfile.trim() || ''; - user.profile.linkedinProfile = req.body.linkedinProfile.trim() || ''; - user.profile.codepenProfile = req.body.codepenProfile.trim() || ''; - user.profile.twitterHandle = req.body.twitterHandle.trim() || ''; - user.profile.bio = req.body.bio.trim() || ''; - user.profile.picture = req.body.picture.trim() || 'https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-180x180.png'; - user.portfolio.website1Title = req.body.website1Title.trim() || ''; - user.portfolio.website1Link = req.body.website1Link.trim() || ''; - user.portfolio.website1Image = req.body.website1Image.trim() || ''; - user.portfolio.website2Title = req.body.website2Title.trim() || ''; - user.portfolio.website2Link = req.body.website2Link.trim() || ''; - user.portfolio.website2Image = req.body.website2Image.trim() || ''; - user.portfolio.website3Title = req.body.website3Title.trim() || ''; - user.portfolio.website3Link = req.body.website3Link.trim() || ''; - user.portfolio.website3Image = req.body.website3Image.trim() || ''; - - user.save(function (err) { + User.findOne({ email: req.body.email }, function(err, existingEmail) { if (err) { return next(err); } - req.flash('success', {msg: 'Profile information updated.'}); - res.redirect('/account'); - resources.updateUserStoryPictures(user._id.toString(), user.profile.picture, user.profile.username); + var user = req.user; + if (existingEmail && existingEmail.email != user.email) { + req.flash('errors', { + msg: "An account with that email address already exists." + }); + return res.redirect('/account'); + } + User.findOne({ username: req.body.username }, function(err, existingUsername) { + if (err) { + return next(err); + } + var user = req.user; + if (existingUsername && existingUsername.profile.username !== user.profile.username) { + req.flash('errors', { + msg: 'An account with that username already exists.' + }); + return res.redirect('/account'); + } + user.email = req.body.email.trim() || ''; + user.profile.name = req.body.name.trim() || ''; + user.profile.username = req.body.username.trim() || ''; + user.profile.location = req.body.location.trim() || ''; + user.profile.githubProfile = req.body.githubProfile.trim() || ''; + user.profile.linkedinProfile = req.body.linkedinProfile.trim() || ''; + user.profile.codepenProfile = req.body.codepenProfile.trim() || ''; + user.profile.twitterHandle = req.body.twitterHandle.trim() || ''; + user.profile.bio = req.body.bio.trim() || ''; + user.profile.picture = req.body.picture.trim() || 'https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-180x180.png'; + user.portfolio.website1Title = req.body.website1Title.trim() || ''; + user.portfolio.website1Link = req.body.website1Link.trim() || ''; + user.portfolio.website1Image = req.body.website1Image.trim() || ''; + user.portfolio.website2Title = req.body.website2Title.trim() || ''; + user.portfolio.website2Link = req.body.website2Link.trim() || ''; + user.portfolio.website2Image = req.body.website2Image.trim() || ''; + user.portfolio.website3Title = req.body.website3Title.trim() || ''; + user.portfolio.website3Link = req.body.website3Link.trim() || ''; + user.portfolio.website3Image = req.body.website3Image.trim() || ''; + + + user.save(function (err) { + if (err) { + return next(err); + } + req.flash('success', {msg: 'Profile information updated.'}); + res.redirect('/account'); + resources.updateUserStoryPictures(user._id.toString(), user.profile.picture, user.profile.username); + }); + }); }); - }); }); - }); }; /** @@ -363,29 +393,29 @@ exports.postUpdateProfile = function(req, res, next) { */ exports.postUpdatePassword = function(req, res, next) { - req.assert('password', 'Password must be at least 4 characters long').len(4); - req.assert('confirmPassword', 'Passwords do not match') - .equals(req.body.password); + req.assert('password', 'Password must be at least 4 characters long').len(4); + req.assert('confirmPassword', 'Passwords do not match') + .equals(req.body.password); - var errors = req.validationErrors(); + var errors = req.validationErrors(); - if (errors) { - req.flash('errors', errors); - return res.redirect('/account'); - } + if (errors) { + req.flash('errors', errors); + return res.redirect('/account'); + } - User.findById(req.user.id, function(err, user) { - if (err) { return next(err); } + User.findById(req.user.id, function(err, user) { + if (err) { return next(err); } - user.password = req.body.password; + user.password = req.body.password; - user.save(function(err) { - if (err) { return next(err); } + user.save(function(err) { + if (err) { return next(err); } - req.flash('success', { msg: 'Password has been changed.' }); - res.redirect('/account'); + req.flash('success', { msg: 'Password has been changed.' }); + res.redirect('/account'); + }); }); - }); }; /** @@ -394,12 +424,12 @@ exports.postUpdatePassword = function(req, res, next) { */ exports.postDeleteAccount = function(req, res, next) { - User.remove({ _id: req.user.id }, function(err) { - if (err) { return next(err); } - req.logout(); - req.flash('info', { msg: 'Your account has been deleted.' }); - res.redirect('/'); - }); + User.remove({ _id: req.user.id }, function(err) { + if (err) { return next(err); } + req.logout(); + req.flash('info', { msg: 'Your account has been deleted.' }); + res.redirect('/'); + }); }; /** @@ -408,22 +438,22 @@ exports.postDeleteAccount = function(req, res, next) { */ exports.getOauthUnlink = function(req, res, next) { - var provider = req.params.provider; - User.findById(req.user.id, function(err, user) { - if (err) { return next(err); } + var provider = req.params.provider; + User.findById(req.user.id, function(err, user) { + if (err) { return next(err); } - user[provider] = undefined; - user.tokens = - _.reject(user.tokens, function(token) { - return token.kind === provider; - }); + user[provider] = undefined; + user.tokens = + _.reject(user.tokens, function(token) { + return token.kind === provider; + }); - user.save(function(err) { - if (err) { return next(err); } - req.flash('info', { msg: provider + ' account has been unlinked.' }); - res.redirect('/account'); + user.save(function(err) { + if (err) { return next(err); } + req.flash('info', { msg: provider + ' account has been unlinked.' }); + res.redirect('/account'); + }); }); - }); }; /** @@ -432,25 +462,25 @@ exports.getOauthUnlink = function(req, res, next) { */ exports.getReset = function(req, res) { - if (req.isAuthenticated()) { - return res.redirect('/'); - } - User - .findOne({ resetPasswordToken: req.params.token }) - .where('resetPasswordExpires').gt(Date.now()) - .exec(function(err, user) { - if (err) { return next(err); } - if (!user) { - req.flash('errors', { - msg: 'Password reset token is invalid or has expired.' + if (req.isAuthenticated()) { + return res.redirect('/'); + } + User + .findOne({ resetPasswordToken: req.params.token }) + .where('resetPasswordExpires').gt(Date.now()) + .exec(function(err, user) { + if (err) { return next(err); } + if (!user) { + req.flash('errors', { + msg: 'Password reset token is invalid or has expired.' + }); + return res.redirect('/forgot'); + } + res.render('account/reset', { + title: 'Password Reset', + token: req.params.token + }); }); - return res.redirect('/forgot'); - } - res.render('account/reset', { - title: 'Password Reset', - token: req.params.token - }); - }); }; /** @@ -459,72 +489,72 @@ exports.getReset = function(req, res) { */ exports.postReset = function(req, res, next) { - var errors = req.validationErrors(); + var errors = req.validationErrors(); - if (errors) { - req.flash('errors', errors); - return res.redirect('back'); - } - - async.waterfall([ - function(done) { - User - .findOne({ resetPasswordToken: req.params.token }) - .where('resetPasswordExpires').gt(Date.now()) - .exec(function(err, user) { - if (err) { return next(err); } - if (!user) { - req.flash('errors', { - msg: 'Password reset token is invalid or has expired.' - }); - return res.redirect('back'); - } - - user.password = req.body.password; - user.resetPasswordToken = undefined; - user.resetPasswordExpires = undefined; - - user.save(function(err) { - if (err) { return done(err); } - req.logIn(user, function(err) { - done(err, user); - }); - }); - }); - }, - function(user, done) { - var transporter = nodemailer.createTransport({ - service: 'Mandrill', - auth: { - user: secrets.mandrill.user, - pass: secrets.mandrill.password - } - }); - var mailOptions = { - to: user.email, - from: 'Team@freecodecamp.com', - subject: 'Your Free Code Camp password has been changed', - text: [ - 'Hello,\n\n', - 'This email is confirming that you requested to', - 'reset your password for your Free Code Camp account.', - 'This is your email:', - user.email, - '\n' - ].join(' ') - }; - transporter.sendMail(mailOptions, function(err) { - if (err) { return done(err); } - req.flash('success', { - msg: 'Success! Your password has been changed.' - }); - done(); - }); + if (errors) { + req.flash('errors', errors); + return res.redirect('back'); } - ], function(err) { - if (err) { return next(err); } - res.redirect('/'); - }); + + async.waterfall([ + function(done) { + User + .findOne({ resetPasswordToken: req.params.token }) + .where('resetPasswordExpires').gt(Date.now()) + .exec(function(err, user) { + if (err) { return next(err); } + if (!user) { + req.flash('errors', { + msg: 'Password reset token is invalid or has expired.' + }); + return res.redirect('back'); + } + + user.password = req.body.password; + user.resetPasswordToken = undefined; + user.resetPasswordExpires = undefined; + + user.save(function(err) { + if (err) { return done(err); } + req.logIn(user, function(err) { + done(err, user); + }); + }); + }); + }, + function(user, done) { + var transporter = nodemailer.createTransport({ + service: 'Mandrill', + auth: { + user: secrets.mandrill.user, + pass: secrets.mandrill.password + } + }); + var mailOptions = { + to: user.email, + from: 'Team@freecodecamp.com', + subject: 'Your Free Code Camp password has been changed', + text: [ + 'Hello,\n\n', + 'This email is confirming that you requested to', + 'reset your password for your Free Code Camp account.', + 'This is your email:', + user.email, + '\n' + ].join(' ') + }; + transporter.sendMail(mailOptions, function(err) { + if (err) { return done(err); } + req.flash('success', { + msg: 'Success! Your password has been changed.' + }); + done(); + }); + } + ], function(err) { + if (err) { return next(err); } + res.redirect('/'); + }); }; /** @@ -533,12 +563,12 @@ exports.postReset = function(req, res, next) { */ exports.getForgot = function(req, res) { - if (req.isAuthenticated()) { - return res.redirect('/'); - } - res.render('account/forgot', { - title: 'Forgot Password' - }); + if (req.isAuthenticated()) { + return res.redirect('/'); + } + res.render('account/forgot', { + title: 'Forgot Password' + }); }; /** @@ -547,80 +577,80 @@ exports.getForgot = function(req, res) { */ exports.postForgot = function(req, res, next) { - var errors = req.validationErrors(); + var errors = req.validationErrors(); - if (errors) { - req.flash('errors', errors); - return res.redirect('/forgot'); - } - - async.waterfall([ - function(done) { - crypto.randomBytes(16, function(err, buf) { - if (err) { return done(err); } - var token = buf.toString('hex'); - done(null, token); - }); - }, - function(token, done) { - User.findOne({ - email: req.body.email.toLowerCase() - }, function(err, user) { - if (err) { return done(err); } - if (!user) { - req.flash('errors', { - msg: 'No account with that email address exists.' - }); - return res.redirect('/forgot'); - } - - user.resetPasswordToken = token; - user.resetPasswordExpires = Date.now() + 3600000; // 1 hour - - user.save(function(err) { - if (err) { return done(err); } - done(null, token, user); - }); - }); - }, - function(token, user, done) { - var transporter = nodemailer.createTransport({ - service: 'Mandrill', - auth: { - user: secrets.mandrill.user, - pass: secrets.mandrill.password - } - }); - var mailOptions = { - to: user.email, - from: 'Team@freecodecamp.com', - subject: 'Reset your Free Code Camp password', - text: [ - 'You are receiving this email because you (or someone else)\n', - 'requested we reset your Free Code Camp account\'s password.\n\n', - 'Please click on the following link, or paste this into your\n', - 'browser to complete the process:\n\n', - 'http://', - req.headers.host, - '/reset/', - token, - '\n\n', - 'If you did not request this, please ignore this email and\n', - 'your password will remain unchanged.\n' - ].join('') - }; - transporter.sendMail(mailOptions, function(err) { - if (err) { return done(err); } - req.flash('info', { - msg: 'An e-mail has been sent to ' + - user.email + - ' with further instructions.' - }); - done(null, 'done'); - }); + if (errors) { + req.flash('errors', errors); + return res.redirect('/forgot'); } - ], function(err) { - if (err) { return next(err); } - res.redirect('/forgot'); - }); + + async.waterfall([ + function(done) { + crypto.randomBytes(16, function(err, buf) { + if (err) { return done(err); } + var token = buf.toString('hex'); + done(null, token); + }); + }, + function(token, done) { + User.findOne({ + email: req.body.email.toLowerCase() + }, function(err, user) { + if (err) { return done(err); } + if (!user) { + req.flash('errors', { + msg: 'No account with that email address exists.' + }); + return res.redirect('/forgot'); + } + + user.resetPasswordToken = token; + user.resetPasswordExpires = Date.now() + 3600000; // 1 hour + + user.save(function(err) { + if (err) { return done(err); } + done(null, token, user); + }); + }); + }, + function(token, user, done) { + var transporter = nodemailer.createTransport({ + service: 'Mandrill', + auth: { + user: secrets.mandrill.user, + pass: secrets.mandrill.password + } + }); + var mailOptions = { + to: user.email, + from: 'Team@freecodecamp.com', + subject: 'Reset your Free Code Camp password', + text: [ + 'You are receiving this email because you (or someone else)\n', + 'requested we reset your Free Code Camp account\'s password.\n\n', + 'Please click on the following link, or paste this into your\n', + 'browser to complete the process:\n\n', + 'http://', + req.headers.host, + '/reset/', + token, + '\n\n', + 'If you did not request this, please ignore this email and\n', + 'your password will remain unchanged.\n' + ].join('') + }; + transporter.sendMail(mailOptions, function(err) { + if (err) { return done(err); } + req.flash('info', { + msg: 'An e-mail has been sent to ' + + user.email + + ' with further instructions.' + }); + done(null, 'done'); + }); + } + ], function(err) { + if (err) { return next(err); } + res.redirect('/forgot'); + }); }; diff --git a/models/Story.js b/models/Story.js index 541bc09169..d5d163ccf1 100644 --- a/models/Story.js +++ b/models/Story.js @@ -14,6 +14,11 @@ var storySchema = new mongoose.Schema({ type: String, unique: false }, + metaDescription: { + type: String, + default: '', + unique: false + }, description: { type: String, unique: false diff --git a/package.json b/package.json index 492406c7b9..e18825f98f 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "passport-twitter": "^1.0.2", "ramda": "^0.10.0", "request": "^2.53.0", + "sanitize-html": "^1.6.1", "sitemap": "^0.7.4", "uglify-js": "^2.4.15", "validator": "^3.22.1", diff --git a/public/css/main.less b/public/css/main.less index 14be443d72..5fc77f37c7 100644 --- a/public/css/main.less +++ b/public/css/main.less @@ -821,6 +821,11 @@ iframe.iphone { height: 50px; } +.url-preview { + max-width: 250px; + max-height: 250px; +} + //.media ~ .media .media-body-wrapper:nth-child(odd) { // background-color: #e5e5e5; //} diff --git a/public/js/main.js b/public/js/main.js index 54ca6e029c..8170c1c297 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -164,7 +164,7 @@ $(document).ready(function() { headline: headline, timePosted: Date.now(), description: description, - + storyMetaDescription: storyMetaDescription, rank: 1, upVotes: [userDataForUpvote], author: { @@ -173,7 +173,7 @@ $(document).ready(function() { username: user.profile.username }, comments: [], - image: '' + image: storyImage } }) .fail(function (xhr, textStatus, errorThrown) { diff --git a/views/account/email-signup.jade b/views/account/email-signup.jade index ab3f821f95..9c4fe0e337 100644 --- a/views/account/email-signup.jade +++ b/views/account/email-signup.jade @@ -40,7 +40,7 @@ block content | Your usernames must be 20 characters or fewer. .form-group .col-sm-6.col-sm-offset-3 - input.form-control(type='password', ng-model='password', name='password', id='password', placeholder='password', required, ng-minlength=5) + input.form-control(type='password', ng-model='password', name='password', id='password', placeholder='password', required, ng-minlength=8) .col-sm-6.col-sm-offset-3(ng-cloak, ng-show="signupForm.password.$error.minlength && !signupForm.password.$pristine") alert(type='danger') span.ion-close-circled diff --git a/views/stories/preliminary-submit.jade b/views/stories/preliminary-submit.jade index d625b8c7ad..31ef779fcd 100644 --- a/views/stories/preliminary-submit.jade +++ b/views/stories/preliminary-submit.jade @@ -34,11 +34,13 @@ }) .done(function (data, textStatus, xhr) { if (data.alreadyPosted) { - window.location = '/stories/' + data.storyURL; + window.location = data.storyURL; } else { - window.location = '/stories/submit/url=' + + window.location = '/stories/submit/new-story?url=' + encodeURIComponent(data.storyURL) + - '&title=' + encodeURIComponent(data.storyTitle); + '&title=' + encodeURIComponent(data.storyTitle) + + '&image=' + encodeURIComponent(data.storyImage) + + '&description=' + encodeURIComponent(data.storyMetaDescription); } }); } diff --git a/views/stories/show.jade b/views/stories/show.jade index a71bb54c9a..2ee65b3290 100644 --- a/views/stories/show.jade +++ b/views/stories/show.jade @@ -5,6 +5,7 @@ var comments = !{JSON.stringify(comments)}; var upVotes = !{JSON.stringify(upVotes)}; var user = !{JSON.stringify(user)}; + var image = !{JSON.stringify(image)}; .spacer h3.row.col-xs-12 @@ -25,7 +26,14 @@ a(href="#{link}") h3= title h6 - .col-xs-12.negative-28 + .col-xs-12.positive-15.hidden-element#image-display + .media + .media-left + img.url-preview.media-object(src="#{image}", alt="#{storyMetaDescription}") + .media-body + .col-xs-12.col-sm-12.col-md-6 + h4= storyMetaDescription + .col-xs-12 h4= description .negative-5 span Posted #{timeAgo} @@ -44,6 +52,9 @@ span.spacer.pull-left#textarea_feedback script. + if (image) { + $('#image-display').removeClass('hidden-element') + } $('#reply-to-main-post').on('click', function() { $('#initial-comment-submit').removeClass('hidden-element'); $(this).unbind('click'); diff --git a/views/stories/submit-story.jade b/views/stories/submit-story.jade index 10cda35788..e8f1fd6801 100644 --- a/views/stories/submit-story.jade +++ b/views/stories/submit-story.jade @@ -3,6 +3,8 @@ script. var storyURL = !{JSON.stringify(storyURL)}; var storyTitle = !{JSON.stringify(storyTitle)}; + var storyImage = !{JSON.stringify(storyImage)}; + var storyMetaDescription = !{JSON.stringify(storyMetaDescription)}; form.form-horizontal.control-label-story-submission#story-submission-form .col-xs-12 .form-group @@ -20,13 +22,29 @@ label.control-label.control-label-story-submission(for='name') Description .col-xs-12.col-md-11 input#description-box.form-control(name="comment-box", placeholder="Start off the discussion with a description of your post" maxlength='140') - span.pull-left#textarea_feedback - .spacer .form-group - button.btn.btn-big.btn-block.btn-primary#story-submit Submit + .col-xs-12.col-md-offset-1 + span.pull-left#textarea_feedback + .form-group + .col-xs-11.col-md-offset-1 + .hidden-element#image-display + .media + .media-left + img.url-preview.media-object(src="#{storyImage}", alt="#{storyMetaDescription}") + .media-body + .col-xs-12 + p= storyMetaDescription + .spacer + .row + .form-group + + button.btn.btn-big.btn-block.btn-primary#story-submit Submit script. $('#story-url').val(storyURL).attr('disabled', 'disabled'); $('#story-title').val(storyTitle); + if (storyImage) { + $('#image-display').removeClass('hidden-element'); + } var text_max = 140; $('#textarea_feedback').html(text_max + ' characters remaining'); $('#description-box').keyup(function () {