From c1ba89c120d7f0aaa46e0115b49a7fb8d6a2cdbb Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Mon, 10 Mar 2014 05:49:48 +0000 Subject: [PATCH 01/16] Bower dependency cleanup issue #2272 - handlebars version should match node (1.3.0) - iCheck isn't used --- Gruntfile.js | 4 ++-- bower.json | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 159b0eed83d..7c775754ed7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -444,7 +444,7 @@ var path = require('path'), 'bower_components/lodash/dist/lodash.underscore.js', 'bower_components/backbone/backbone.js', - 'bower_components/handlebars.js/dist/handlebars.runtime.js', + 'bower_components/handlebars/handlebars.runtime.js', 'bower_components/moment/moment.js', 'bower_components/jquery-file-upload/js/jquery.fileupload.js', 'bower_components/codemirror/lib/codemirror.js', @@ -500,7 +500,7 @@ var path = require('path'), 'bower_components/lodash/dist/lodash.underscore.js', 'bower_components/backbone/backbone.js', - 'bower_components/handlebars.js/dist/handlebars.runtime.js', + 'bower_components/handlebars/handlebars.runtime.js', 'bower_components/moment/moment.js', 'bower_components/jquery-file-upload/js/jquery.fileupload.js', 'bower_components/codemirror/lib/codemirror.js', diff --git a/bower.json b/bower.json index 7d57a110e78..86deae256e9 100644 --- a/bower.json +++ b/bower.json @@ -6,8 +6,7 @@ "Countable": "2.0.2", "fastclick": "1.0.0", "ghost-ui": "0.1.0", - "handlebars.js": "1.0.0", - "iCheck": "1.0.1", + "handlebars": "1.3.0", "jquery": "1.11.0", "jquery-file-upload": "9.5.6", "jquery-hammerjs": "1.0.1", From dd2a1dd6392321bde6ecafc09d06e2ab2332e0af Mon Sep 17 00:00:00 2001 From: John O'Nolan Date: Mon, 10 Mar 2014 16:33:46 +0100 Subject: [PATCH 02/16] Clean up markdown help modal * Shorter, more user friendly. * See #1463 - Not fixing in any way, just related. --- core/client/tpl/modals/markdown.hbs | 51 ++--------------------------- 1 file changed, 3 insertions(+), 48 deletions(-) diff --git a/core/client/tpl/modals/markdown.hbs b/core/client/tpl/modals/markdown.hbs index b758838d0a2..b7e8b228e36 100644 --- a/core/client/tpl/modals/markdown.hbs +++ b/core/client/tpl/modals/markdown.hbs @@ -18,11 +18,6 @@ *text* Ctrl / Cmd + I - - Inline Code - `code` - Cmd + K / Ctrl + Shift + K - Strike-through ~~text~~ @@ -64,49 +59,9 @@ Ctrl + Alt + 3 - H4 - #### Heading - Ctrl + Alt + 4 - - - H5 - ##### Heading - Ctrl + Alt + 5 - - - H6 - ###### Heading - Ctrl + Alt + 6 - - - Select Word - - Ctrl + Alt + W - - - New Paragraph - - Ctrl / Cmd + Enter - - - Uppercase - - Ctrl + U - - - Lowercase - - Ctrl + Shift + U - - - Titlecase - - Ctrl + Alt + Shift + U - - - Insert Current Date - - Ctrl + Shift + 1 + Inline Code + `code` + Cmd + K / Ctrl + Shift + K From c917c0f0eb5a3acf8c6770ffcd4e6bda5c508be0 Mon Sep 17 00:00:00 2001 From: Kyle Nunery Date: Mon, 10 Mar 2014 11:42:38 -0400 Subject: [PATCH 03/16] Blog post titles will now be properly escaped in rss (xml) feeds (reopens #715) Closes #2313 --- core/server/controllers/frontend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/server/controllers/frontend.js b/core/server/controllers/frontend.js index 49c7c70de60..0d8b8b390dc 100644 --- a/core/server/controllers/frontend.js +++ b/core/server/controllers/frontend.js @@ -301,7 +301,7 @@ frontendControllers = { posts.forEach(function (post) { var deferred = when.defer(), item = { - title: _.escape(post.title), + title: post.title, guid: post.uuid, url: config.urlFor('post', {post: post, permalinks: permalinks}, true), date: post.published_at, From 3e21940b183178f81b4ffe1391434e74562b313e Mon Sep 17 00:00:00 2001 From: Harry Wolff Date: Wed, 19 Feb 2014 22:22:02 -0500 Subject: [PATCH 04/16] Add promise to ghost startup process to allow hooking into when ghost has finished loading addresses item 9 in #2078 and makes progress on #2182 - has files that startup ghost return a promise that is resolved once ghost has finished loading - moves getSocket into config file - removes models.reset() as it's not used anywhere - update functions in server startup - remove unused version hash variable --- core/index.js | 22 +++- core/server/config/index.js | 9 ++ core/server/index.js | 219 +++++++++++++++++------------------- core/server/models/index.js | 5 - index.js | 7 +- 5 files changed, 138 insertions(+), 124 deletions(-) diff --git a/core/index.js b/core/index.js index e65a0726094..28932730334 100644 --- a/core/index.js +++ b/core/index.js @@ -2,17 +2,31 @@ // Orchestrates the loading of Ghost // When run from command line. -var bootstrap = require('./bootstrap'), - errors = require('./server/errorHandling'); +var when = require('when'), + bootstrap = require('./bootstrap'); process.env.NODE_ENV = process.env.NODE_ENV || 'development'; function startGhost(options) { + // When we no longer need to require('./server') + // in a callback this extra deferred object + // won't be necessary, we'll just be able to return + // the server object directly. + var deferred = when.defer(); + options = options || {}; + bootstrap(options.config).then(function () { var ghost = require('./server'); - ghost(options.app); - }).otherwise(errors.logAndThrowError); + return ghost(options.app).then(deferred.resolve).otherwise(function (e) { + // We don't return the rejected promise to stop + // the propogation of the rejection and just + // allow the user to manage what to do. + deferred.reject(e); + }); + }); + + return deferred.promise; } module.exports = startGhost; \ No newline at end of file diff --git a/core/server/config/index.js b/core/server/config/index.js index 8bcfa90d0ac..cee5842865e 100644 --- a/core/server/config/index.js +++ b/core/server/config/index.js @@ -14,6 +14,14 @@ var path = require('path'), appRoot = path.resolve(__dirname, '../../../'), corePath = path.resolve(appRoot, 'core/'); +// Are we using sockets? Custom socket or the default? +function getSocket() { + if (ghostConfig.server.hasOwnProperty('socket')) { + return _.isString(ghostConfig.server.socket) ? ghostConfig.server.socket : path.join(ghostConfig.paths.contentPath, process.env.NODE_ENV + '.socket'); + } + return false; +} + function updateConfig(config) { var localPath, contentPath, @@ -110,5 +118,6 @@ function config() { module.exports = config; module.exports.init = initConfig; module.exports.theme = theme; +module.exports.getSocket = getSocket; module.exports.urlFor = configUrl.urlFor; module.exports.urlForPost = configUrl.urlForPost; diff --git a/core/server/index.js b/core/server/index.js index e417a990ed7..3d559dcef54 100644 --- a/core/server/index.js +++ b/core/server/index.js @@ -4,7 +4,6 @@ var crypto = require('crypto'), hbs = require('express-hbs'), fs = require('fs'), uuid = require('node-uuid'), - path = require('path'), Polyglot = require('node-polyglot'), semver = require('semver'), _ = require('lodash'), @@ -22,7 +21,6 @@ var crypto = require('crypto'), routes = require('./routes'), packageInfo = require('../../package.json'), - // Variables dbHash; @@ -107,21 +105,92 @@ function builtFilesExist() { return when.all(deferreds); } +function startGhost(deferred) { + + return function () { + // Tell users if their node version is not supported, and exit + if (!semver.satisfies(process.versions.node, packageInfo.engines.node)) { + console.log( + "\nERROR: Unsupported version of Node".red, + "\nGhost needs Node version".red, + packageInfo.engines.node.yellow, + "you are using version".red, + process.versions.node.yellow, + "\nPlease go to http://nodejs.org to get a supported version".green + ); + + process.exit(0); + } + + // Startup & Shutdown messages + if (process.env.NODE_ENV === 'production') { + console.log( + "Ghost is running...".green, + "\nYour blog is now available on", + config().url, + "\nCtrl+C to shut down".grey + ); + + // ensure that Ghost exits correctly on Ctrl+C + process.on('SIGINT', function () { + console.log( + "\nGhost has shut down".red, + "\nYour blog is now offline" + ); + process.exit(0); + }); + } else { + console.log( + ("Ghost is running in " + process.env.NODE_ENV + "...").green, + "\nListening on", + config.getSocket() || config().server.host + ':' + config().server.port, + "\nUrl configured as:", + config().url, + "\nCtrl+C to shut down".grey + ); + // ensure that Ghost exits correctly on Ctrl+C + process.on('SIGINT', function () { + console.log( + "\nGhost has shutdown".red, + "\nGhost was running for", + Math.round(process.uptime()), + "seconds" + ); + process.exit(0); + }); + } + + deferred.resolve(); + }; +} + +// ## Initializes the ghost application. // Sets up the express server instance. // Instantiates the ghost singleton, helpers, routes, middleware, and apps. // Finally it starts the http server. -function setup(server) { - +function init(server) { // create a hash for cache busting assets var assetHash = (crypto.createHash('md5').update(packageInfo.version + Date.now()).digest('hex')).substring(0, 10); + // If no express instance is passed in + // then create our own + if (!server) { + server = express(); + } + // Set up Polygot instance on the require module Polyglot.instance = new Polyglot(); // ### Initialisation + // The server and its dependencies require a populated config + // It returns a promise that is resolved when the application + // has finished starting up. - // Initialise the models - models.init().then(function () { + // Make sure javascript files have been built via grunt concat + return builtFilesExist().then(function () { + // Initialise the models + return models.init(); + }).then(function () { // Populate any missing default settings return models.Settings.populateDefaults(); }).then(function () { @@ -136,19 +205,17 @@ function setup(server) { // Check for or initialise a dbHash. initDbHashAndFirstRun(), // Initialize the permissions actions and objects - permissions.init() + permissions.init(), + // Initialize mail + mailer.init(), + // Initialize apps + apps.init() ); }).then(function () { - // Make sure javascript files have been built via grunt concat - return builtFilesExist(); - }).then(function () { - // Initialize mail - return mailer.init(); - }).then(function () { - var adminHbs = hbs.create(); + var adminHbs = hbs.create(), + deferred = when.defer(); // ##Configuration - server.set('version hash', assetHash); // return the correct mime type for woff filess express['static'].mime.define({'application/font-woff': ['woff']}); @@ -177,111 +244,37 @@ function setup(server) { // Set up Frontend routes routes.frontend(server); - // Are we using sockets? Custom socket or the default? - function getSocket() { - if (config().server.hasOwnProperty('socket')) { - return _.isString(config().server.socket) ? config().server.socket : path.join(config.path().contentPath, process.env.NODE_ENV + '.socket'); - } - return false; - } - - function startGhost() { - // Tell users if their node version is not supported, and exit - if (!semver.satisfies(process.versions.node, packageInfo.engines.node)) { - console.log( - "\nERROR: Unsupported version of Node".red, - "\nGhost needs Node version".red, - packageInfo.engines.node.yellow, - "you are using version".red, - process.versions.node.yellow, - "\nPlease go to http://nodejs.org to get a supported version".green - ); - - process.exit(0); - } - - // Startup & Shutdown messages - if (process.env.NODE_ENV === 'production') { - console.log( - "Ghost is running...".green, - "\nYour blog is now available on", - config().url, - "\nCtrl+C to shut down".grey - ); - - // ensure that Ghost exits correctly on Ctrl+C - process.on('SIGINT', function () { - console.log( - "\nGhost has shut down".red, - "\nYour blog is now offline" - ); - process.exit(0); - }); - } else { - console.log( - ("Ghost is running in " + process.env.NODE_ENV + "...").green, - "\nListening on", - getSocket() || config().server.host + ':' + config().server.port, - "\nUrl configured as:", - config().url, - "\nCtrl+C to shut down".grey - ); - // ensure that Ghost exits correctly on Ctrl+C - process.on('SIGINT', function () { - console.log( - "\nGhost has shutdown".red, - "\nGhost was running for", - Math.round(process.uptime()), - "seconds" - ); - process.exit(0); - }); - } - - } + // Log all theme errors and warnings + _.each(config().paths.availableThemes._messages.errors, function (error) { + errors.logError(error.message, error.context); + }); - // Initialize apps then start the server - apps.init().then(function () { - - // ## Start Ghost App - if (getSocket()) { - // Make sure the socket is gone before trying to create another - fs.unlink(getSocket(), function (err) { - /*jshint unused:false*/ - server.listen( - getSocket(), - startGhost - ); - fs.chmod(getSocket(), '0660'); - }); + _.each(config().paths.availableThemes._messages.warns, function (warn) { + errors.logWarn(warn.message, warn.context); + }); - } else { + // ## Start Ghost App + if (config.getSocket()) { + // Make sure the socket is gone before trying to create another + fs.unlink(config.getSocket(), function (err) { + /*jshint unused:false*/ server.listen( - config().server.port, - config().server.host, - startGhost + config.getSocket(), + startGhost(deferred) ); - } - _.each(config().paths.availableThemes._messages.errors, function (error) { - errors.logError(error.message, error.context); + fs.chmod(config.getSocket(), '0660'); }); - _.each(config().paths.availableThemes._messages.warns, function (warn) { - errors.logWarn(warn.message, warn.context); - }); - }); - }, function (err) { - errors.logErrorAndExit(err, err.context, err.help); - }); -} -// Initializes the ghost application. -function init(app) { - if (!app) { - app = express(); - } + } else { + server.listen( + config().server.port, + config().server.host, + startGhost(deferred) + ); + } - // The server and its dependencies require a populated config - setup(app); + return deferred.promise; + }); } module.exports = init; diff --git a/core/server/models/index.js b/core/server/models/index.js index f0b764d8c59..16de3246ef4 100644 --- a/core/server/models/index.js +++ b/core/server/models/index.js @@ -14,11 +14,6 @@ module.exports = { init: function () { return migrations.init(); }, - reset: function () { - return migrations.reset().then(function () { - return migrations.init(); - }); - }, // ### deleteAllContent // Delete all content from the database (posts, tags, tags_posts) deleteAllContent: function () { diff --git a/index.js b/index.js index 6fb52b3b6d2..6cfca38bc02 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,9 @@ // Orchestrates the loading of Ghost // When run from command line. -var ghost = require('./core'); +var ghost = require('./core'), + errors = require('./core/server/errorHandling'); -ghost(); \ No newline at end of file +ghost().otherwise(function (err) { + errors.logErrorAndExit(err, err.context, err.help); +}); \ No newline at end of file From 373c798b8d65a467f38676a8f7ce807126c0a6a2 Mon Sep 17 00:00:00 2001 From: Manuel Mitasch Date: Tue, 11 Mar 2014 16:50:29 +0100 Subject: [PATCH 05/16] Adding .bowerrc If no .bowerrc file is found in the current folder it seems to lookup if one exists in parent folders. Thus, we need to use .bowerrc in order to avoid problems. --- .bowerrc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .bowerrc diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 00000000000..df4bceeceb9 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "bower_components" +} \ No newline at end of file From a806f3e0972dbb45080eb7131628676d871de0f0 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Wed, 12 Mar 2014 21:29:36 +0000 Subject: [PATCH 06/16] Updating ghost-ui --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 86deae256e9..ffea8a57496 100644 --- a/bower.json +++ b/bower.json @@ -5,7 +5,7 @@ "codemirror": "3.15.0", "Countable": "2.0.2", "fastclick": "1.0.0", - "ghost-ui": "0.1.0", + "ghost-ui": "0.1.2", "handlebars": "1.3.0", "jquery": "1.11.0", "jquery-file-upload": "9.5.6", From 36d38e5c81d1472ee2171bb81a149bab24eacbad Mon Sep 17 00:00:00 2001 From: mattse Date: Wed, 12 Mar 2014 17:31:57 -0400 Subject: [PATCH 07/16] Added functional test for uploading image/cover in settings references #2273 - added a test that clicks on both upload (image/cover) buttons and tests the same testing function on them since both modals are exactly the same - the testing function checks for the '.js-drop-zone.image-uploader' selector, then clicks accept, and tests that a blank success notification appears - shifted the test email test validation upwards so the ordering of tests matches the ordering of the UI elements --- core/test/functional/admin/settings_test.js | 129 +++++++++++++------- 1 file changed, 83 insertions(+), 46 deletions(-) diff --git a/core/test/functional/admin/settings_test.js b/core/test/functional/admin/settings_test.js index ea6bfa6b88d..48ec2d50bba 100644 --- a/core/test/functional/admin/settings_test.js +++ b/core/test/functional/admin/settings_test.js @@ -154,6 +154,89 @@ CasperTest.begin('Ensure general blog description field length validation', 3, f }, 2000); }); +CasperTest.begin('Ensure image upload modals display correctly', 6, function suite(test) { + casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time"); + }); + + function assertImageUploaderModalThenClose() { + test.assertExists('.js-drop-zone.image-uploader', 'Image drop zone modal renders correctly'); + this.click('#modal-container .js-button-accept'); + casper.waitForSelector('.notification-success', function onSuccess() { + test.assert(true, 'Got success notification'); + }, function onTimeout() { + test.fail('No success notification'); + }, 1000); + }; + + // Test Blog Logo Upload Button + casper.waitForSelector('#general', function then() { + this.click('#general .js-modal-logo'); + }); + + casper.waitForSelector('#modal-container .modal-content', assertImageUploaderModalThenClose, + function onTimeout() { + test.fail('No upload logo modal container appeared'); + }, 1000); + + // Test Blog Cover Upload Button + casper.then(function() { + this.click('#general .js-modal-cover'); + }); + + casper.waitForSelector('#modal-container .modal-content', assertImageUploaderModalThenClose, + function onTimeout() { + test.fail('No upload cover modal container appeared'); + }, 1000); +}); + +CasperTest.begin("User settings screen validates email", 6, function suite(test) { + var email, brokenEmail; + + casper.thenOpen(url + "ghost/settings/user/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + test.assertUrlMatch(/ghost\/settings\/user\/$/, "Ghost doesn't require login this time"); + }); + + casper.then(function setEmailToInvalid() { + email = casper.getElementInfo('#user-email').attributes.value; + brokenEmail = email.replace('.', '-'); + + casper.fillSelectors('.user-profile', { + '#user-email': brokenEmail + }, false); + }); + + casper.thenClick('#user .button-save'); + + casper.waitForResource('/users/'); + + casper.waitForSelector('.notification-error', function onSuccess() { + test.assert(true, 'Got error notification'); + test.assertSelectorDoesntHaveText('.notification-error', '[object Object]'); + }, function onTimeout() { + test.assert(false, 'No error notification :('); + }); + + casper.then(function resetEmailToValid() { + casper.fillSelectors('.user-profile', { + '#user-email': email + }, false); + }); + + casper.thenClick('#user .button-save'); + + casper.waitForResource(/users/); + + casper.waitForSelector('.notification-success', function onSuccess() { + test.assert(true, 'Got success notification'); + test.assertSelectorDoesntHaveText('.notification-success', '[object Object]'); + }, function onTimeout() { + test.assert(false, 'No success notification :('); + }); +}); + CasperTest.begin('Ensure postsPerPage number field form validation', 3, function suite(test) { casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() { test.assertTitle("Ghost Admin", "Ghost admin has no title"); @@ -217,52 +300,6 @@ CasperTest.begin('Ensure postsPerPage min of 0', 3, function suite(test) { }, 2000); }); -CasperTest.begin("User settings screen validates email", 6, function suite(test) { - var email, brokenEmail; - - casper.thenOpen(url + "ghost/settings/user/", function testTitleAndUrl() { - test.assertTitle("Ghost Admin", "Ghost admin has no title"); - test.assertUrlMatch(/ghost\/settings\/user\/$/, "Ghost doesn't require login this time"); - }); - - casper.then(function setEmailToInvalid() { - email = casper.getElementInfo('#user-email').attributes.value; - brokenEmail = email.replace('.', '-'); - - casper.fillSelectors('.user-profile', { - '#user-email': brokenEmail - }, false); - }); - - casper.thenClick('#user .button-save'); - - casper.waitForResource('/users/'); - - casper.waitForSelector('.notification-error', function onSuccess() { - test.assert(true, 'Got error notification'); - test.assertSelectorDoesntHaveText('.notification-error', '[object Object]'); - }, function onTimeout() { - test.assert(false, 'No error notification :('); - }); - - casper.then(function resetEmailToValid() { - casper.fillSelectors('.user-profile', { - '#user-email': email - }, false); - }); - - casper.thenClick('#user .button-save'); - - casper.waitForResource(/users/); - - casper.waitForSelector('.notification-success', function onSuccess() { - test.assert(true, 'Got success notification'); - test.assertSelectorDoesntHaveText('.notification-success', '[object Object]'); - }, function onTimeout() { - test.assert(false, 'No success notification :('); - }); -}); - CasperTest.begin("User settings screen shows remaining characters for Bio properly", 4, function suite(test) { function getRemainingBioCharacterCount() { From 4556e1df0a6d116f178ef24195298ac4f768d4bf Mon Sep 17 00:00:00 2001 From: Johan Stenehall Date: Wed, 5 Mar 2014 23:03:39 +0100 Subject: [PATCH 08/16] Rss support for tags closes #2260 - added routes for /tag/:slug/rss and /tag/:slug/rss/:page - added support for tag in the rss controller - added route tests for each extra case - fixing a tiny typo in some test descriptions --- core/server/controllers/frontend.js | 65 +++++++++++++------- core/server/routes/frontend.js | 2 + core/test/functional/routes/frontend_test.js | 55 +++++++++++++++-- 3 files changed, 94 insertions(+), 28 deletions(-) diff --git a/core/server/controllers/frontend.js b/core/server/controllers/frontend.js index 0d8b8b390dc..7c220e03250 100644 --- a/core/server/controllers/frontend.js +++ b/core/server/controllers/frontend.js @@ -252,11 +252,16 @@ frontendControllers = { 'rss': function (req, res, next) { // Initialize RSS var pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1, - feed; + tagParam = req.params.slug; // No negative pages, or page 1 - if (isNaN(pageParam) || pageParam < 1 || (pageParam === 1 && req.route.path === '/rss/:page/')) { - return res.redirect(config().paths.subdir + '/rss/'); + if (isNaN(pageParam) || pageParam < 1 || + (pageParam === 1 && (req.route.path === '/rss/:page/' || req.route.path === '/tag/:slug/rss/:page/'))) { + if (tagParam !== undefined) { + return res.redirect(config().paths.subdir + '/tag/' + tagParam + '/rss/'); + } else { + return res.redirect(config().paths.subdir + '/rss/'); + } } // TODO: needs refactor for multi user to not use first user as default @@ -266,25 +271,37 @@ frontendControllers = { api.settings.read('description'), api.settings.read('permalinks') ]).then(function (result) { - var user = result[0].value, - title = result[1].value.value, - description = result[2].value.value, - permalinks = result[3].value, - siteUrl = config.urlFor('home', null, true), - feedUrl = config.urlFor('rss', null, true); - - feed = new RSS({ - title: title, - description: description, - generator: 'Ghost v' + res.locals.version, - feed_url: feedUrl, - site_url: siteUrl, - ttl: '60' - }); - return api.posts.browse({page: pageParam}).then(function (page) { - var maxPage = page.pages, - feedItems = []; + var options = {}; + if (pageParam) { options.page = pageParam; } + if (tagParam) { options.tag = tagParam; } + + return api.posts.browse(options).then(function (page) { + + var user = result[0].value, + title = result[1].value.value, + description = result[2].value.value, + permalinks = result[3].value, + siteUrl = config.urlFor('home', null, true), + feedUrl = config.urlFor('rss', null, true), + maxPage = page.pages, + feedItems = [], + feed; + + if (tagParam) { + title = page.aspect.tag.name + ' - ' + title; + feedUrl = feedUrl + 'tag/' + page.aspect.tag.slug + '/'; + } + + feed = new RSS({ + title: title, + description: description, + generator: 'Ghost v' + res.locals.version, + feed_url: feedUrl, + site_url: siteUrl, + ttl: '60' + }); + // A bit of a hack for situations with no content. if (maxPage === 0) { @@ -294,7 +311,11 @@ frontendControllers = { // If page is greater than number of pages we have, redirect to last page if (pageParam > maxPage) { - return res.redirect(config().paths.subdir + '/rss/' + maxPage + '/'); + if (tagParam) { + return res.redirect(config().paths.subdir + '/tag/' + tagParam + '/rss/' + maxPage + '/'); + } else { + return res.redirect(config().paths.subdir + '/rss/' + maxPage + '/'); + } } filters.doFilter('prePostsRender', page.posts).then(function (posts) { diff --git a/core/server/routes/frontend.js b/core/server/routes/frontend.js index f3149b195a6..84aa5cf358e 100644 --- a/core/server/routes/frontend.js +++ b/core/server/routes/frontend.js @@ -6,6 +6,8 @@ module.exports = function (server) { // ### Frontend routes server.get('/rss/', frontend.rss); server.get('/rss/:page/', frontend.rss); + server.get('/tag/:slug/rss/', frontend.rss); + server.get('/tag/:slug/rss/:page/', frontend.rss); server.get('/tag/:slug/page/:page/', frontend.tag); server.get('/tag/:slug/', frontend.tag); server.get('/page/:page/', frontend.homepage); diff --git a/core/test/functional/routes/frontend_test.js b/core/test/functional/routes/frontend_test.js index dddab36c58f..01be21894aa 100644 --- a/core/test/functional/routes/frontend_test.js +++ b/core/test/functional/routes/frontend_test.js @@ -171,7 +171,7 @@ describe('Frontend Routing', function () { .end(doEnd(done)); }); - it('should redirect to last page is page too high', function (done) { + it('should redirect to last page if page too high', function (done) { request.get('/page/4/') .expect('Location', '/page/3/') .expect('Cache-Control', cacheRules['public']) @@ -179,7 +179,7 @@ describe('Frontend Routing', function () { .end(doEnd(done)); }); - it('should redirect to first page is page too low', function (done) { + it('should redirect to first page if page too low', function (done) { request.get('/page/0/') .expect('Location', '/') .expect('Cache-Control', cacheRules['public']) @@ -214,7 +214,7 @@ describe('Frontend Routing', function () { .end(doEnd(done)); }); - it('should redirect to last page is page too high', function (done) { + it('should redirect to last page if page too high', function (done) { request.get('/rss/3/') .expect('Location', '/rss/2/') .expect('Cache-Control', cacheRules['public']) @@ -222,7 +222,7 @@ describe('Frontend Routing', function () { .end(doEnd(done)); }); - it('should redirect to first page is page too low', function (done) { + it('should redirect to first page if page too low', function (done) { request.get('/rss/0/') .expect('Location', '/rss/') .expect('Cache-Control', cacheRules['public']) @@ -231,6 +231,49 @@ describe('Frontend Routing', function () { }); }); + describe('Tag based RSS pages', function () { + it('should redirect without slash', function (done) { + request.get('/tag/getting-started/rss') + .expect('Location', '/tag/getting-started/rss/') + .expect('Cache-Control', cacheRules.year) + .expect(301) + .end(doEnd(done)); + }); + + it('should respond with xml', function (done) { + request.get('/tag/getting-started/rss/') + .expect('Content-Type', /xml/) + .expect('Cache-Control', cacheRules['public']) + .expect(200) + .end(doEnd(done)); + }); + + it('should redirect page 1', function (done) { + request.get('/tag/getting-started/rss/1/') + .expect('Location', '/tag/getting-started/rss/') + .expect('Cache-Control', cacheRules['public']) + // TODO: This should probably be a 301? + .expect(302) + .end(doEnd(done)); + }); + + it('should redirect to last page if page too high', function (done) { + request.get('/tag/getting-started/rss/2/') + .expect('Location', '/tag/getting-started/rss/1/') + .expect('Cache-Control', cacheRules['public']) + .expect(302) + .end(doEnd(done)); + }); + + it('should redirect to first page if page too low', function (done) { + request.get('/tag/getting-started/rss/0/') + .expect('Location', '/tag/getting-started/rss/') + .expect('Cache-Control', cacheRules['public']) + .expect(302) + .end(doEnd(done)); + }); + }); + describe('Static page', function () { it('should redirect without slash', function (done) { request.get('/static-page-test') @@ -335,7 +378,7 @@ describe('Frontend Routing', function () { .end(doEnd(done)); }); - it('should redirect to last page is page too high', function (done) { + it('should redirect to last page if page too high', function (done) { request.get('/tag/injection/page/4/') .expect('Location', '/tag/injection/page/2/') .expect('Cache-Control', cacheRules['public']) @@ -343,7 +386,7 @@ describe('Frontend Routing', function () { .end(doEnd(done)); }); - it('should redirect to first page is page too low', function (done) { + it('should redirect to first page if page too low', function (done) { request.get('/tag/injection/page/0/') .expect('Location', '/tag/injection/') .expect('Cache-Control', cacheRules['public']) From 959c018fab35759763de3a8e6b56f6a69dbec5c1 Mon Sep 17 00:00:00 2001 From: Rob Graeber Date: Thu, 13 Mar 2014 09:46:24 -0700 Subject: [PATCH 09/16] Updating copyright range Updating the copyright year to be a range. :-) Otherwise the license wouldn't explicit cover the older versions you've made. --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 06189940a23..8d0dbdd5932 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014 Ghost Foundation - Released under The MIT License. +Copyright (c) 2013-2014 Ghost Foundation - Released under The MIT License. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation @@ -19,4 +19,4 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +OTHER DEALINGS IN THE SOFTWARE. From c3417fe0907caa697fbb28798d9594ab07c59a1f Mon Sep 17 00:00:00 2001 From: Fabian Becker Date: Sun, 9 Mar 2014 09:28:58 +0000 Subject: [PATCH 10/16] Serve default robots.txt closes #2062 - Server robots.txt from theme if available - Serve default robots.txt from /core/shared/ otherwise - Added tests for default robots.txt --- core/server/middleware/index.js | 39 +++++++++++++++++++- core/test/functional/routes/frontend_test.js | 7 ++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/core/server/middleware/index.js b/core/server/middleware/index.js index 26bcebeac4a..285e4a71b17 100644 --- a/core/server/middleware/index.js +++ b/core/server/middleware/index.js @@ -194,6 +194,41 @@ function checkSSL(req, res, next) { next(); } +// ### Robots Middleware +// Handle requests to robots.txt and cache file +function robots() { + var content, // file cache + filePath = path.join(config().paths.corePath, '/shared/robots.txt'); + + return function robots(req, res, next) { + if ('/robots.txt' === req.url) { + if (content) { + res.writeHead(200, content.headers); + res.end(content.body); + } else { + fs.readFile(filePath, function (err, buf) { + if (err) { + return next(err); + } + + content = { + headers: { + 'Content-Type': 'text/plain', + 'Content-Length': buf.length, + 'Cache-Control': 'public, max-age=' + ONE_YEAR_MS / 1000 + }, + body: buf + }; + res.writeHead(200, content.headers); + res.end(content.body); + }); + } + } else { + next(); + } + }; +} + module.exports = function (server, dbHash) { var logging = config().logging, subdir = config().paths.subdir, @@ -229,7 +264,6 @@ module.exports = function (server, dbHash) { // First determine whether we're serving admin or theme content expressServer.use(manageAdminAndTheme); - // Admin only config expressServer.use(subdir + '/ghost', middleware.whenEnabled('admin', express['static'](path.join(corePath, '/client/assets'), {maxAge: ONE_YEAR_MS}))); @@ -242,6 +276,9 @@ module.exports = function (server, dbHash) { // Theme only config expressServer.use(subdir, middleware.whenEnabled(expressServer.get('activeTheme'), middleware.staticTheme())); + // Serve robots.txt if not found in theme + expressServer.use(robots()); + // Add in all trailing slashes expressServer.use(slashes(true, {headers: {'Cache-Control': 'public, max-age=' + ONE_YEAR_S}})); diff --git a/core/test/functional/routes/frontend_test.js b/core/test/functional/routes/frontend_test.js index dddab36c58f..351d06c6508 100644 --- a/core/test/functional/routes/frontend_test.js +++ b/core/test/functional/routes/frontend_test.js @@ -282,6 +282,13 @@ describe('Frontend Routing', function () { .end(doEnd(done)); }); + it('should retrieve default robots.txt', function (done) { + request.get('/robots.txt') + .expect('Cache-Control', cacheRules.year) + .expect(200) + .end(doEnd(done)); + }) + // at the moment there is no image fixture to test // it('should retrieve image assets', function (done) { // request.get('/content/images/some.jpg') From b605f8b75a015e5bfcb9536a67ca44d8c9d32785 Mon Sep 17 00:00:00 2001 From: John O'Nolan Date: Thu, 13 Mar 2014 22:43:05 +0100 Subject: [PATCH 11/16] Update copyright --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 63eb30a1767..a5b046387f8 100644 --- a/README.md +++ b/README.md @@ -83,4 +83,4 @@ Constructed with the following guidelines: ## Copyright & License -Copyright (C) 2014 Ghost Foundation - Released under the [MIT license](LICENSE). +Copyright (c) 2013-2014 Ghost Foundation - Released under the [MIT license](LICENSE). From 8ea0b8a5d19533f09cc75fef691e061e0f4e467e Mon Sep 17 00:00:00 2001 From: John O'Nolan Date: Thu, 13 Mar 2014 23:47:46 +0100 Subject: [PATCH 12/16] Moar iOS mobile friendly fandango * Minimalise up in this what http://visuellegedanken.de/2014-03-13/viewport-meta-tag-minimal-ui/ * Webapp capable all over the hello * Consistent self-closing meta tags and shit This needs to be merged into the ember branch. Probably. --- core/server/views/default.hbs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/core/server/views/default.hbs b/core/server/views/default.hbs index 898205d0d94..24428df33fc 100644 --- a/core/server/views/default.hbs +++ b/core/server/views/default.hbs @@ -4,31 +4,33 @@ - + Ghost Admin - - - + + + - + + + - + - - + + - - + + {{#unless hideNavbar}} From 1d5a8ce718bfb045e2f0008878c42a1150a8e77f Mon Sep 17 00:00:00 2001 From: mattse Date: Thu, 13 Mar 2014 20:58:09 -0400 Subject: [PATCH 13/16] Added functional tests for editor: tag editor, image uploads, post settings references #2273 - test tag creation and tag deletion - tests image uploader appears after typing `![]()` in editor - tests image URL matches url inside `![](url)` - tests all input elements of post settings menu --- core/test/functional/admin/editor_test.js | 186 ++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/core/test/functional/admin/editor_test.js b/core/test/functional/admin/editor_test.js index 5ae016b9ea9..cbe8e98aeb5 100644 --- a/core/test/functional/admin/editor_test.js +++ b/core/test/functional/admin/editor_test.js @@ -91,6 +91,81 @@ CasperTest.begin("Word count and plurality", 4, function suite(test) { }); }); +CasperTest.begin("Image Uploads", 14, function suite(test) { + casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + }); + + // Test standard image upload modal + casper.then(function () { + casper.writeContentToCodeMirror("![]()"); + }); + + function assertEmptyImageUploaderDisplaysCorrectly() { + test.assertExists(".entry-preview .js-upload-target", "Upload target exists"); + test.assertExists(".entry-preview .js-fileupload", "File upload target exists"); + test.assertExists(".entry-preview .image-url", "Image URL button exists"); + }; + + casper.waitForSelector(".entry-preview .js-drop-zone.image-uploader", assertEmptyImageUploaderDisplaysCorrectly); + + // Test image URL upload modal + casper.thenClick(".entry-preview .image-uploader a.image-url"); + + casper.waitForSelector(".image-uploader-url", function onSuccess() { + test.assertExists(".image-uploader-url .url.js-upload-url", "Image URL uploader exists") + test.assertExists(".image-uploader-url .button-save.js-button-accept", "Image URL accept button exists") + test.assertExists(".image-uploader-url .image-upload", "Back to normal image upload style button exists") + }); + + // Test image source location + casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + }); + + var testFileLocation = "test/file/location"; + + casper.then(function () { + var markdownImageString = "![](" + testFileLocation + ")"; + casper.writeContentToCodeMirror(markdownImageString); + }); + + casper.waitForSelector(".entry-preview .js-drop-zone.pre-image-uploader", function onSuccess() { + var imageJQuerySelector = ".entry-preview img.js-upload-target[src='" + testFileLocation + "']" + test.assertExists(imageJQuerySelector, "Uploaded image tag properly links to source location"); + }); + + // Test cancel image button + casper.thenClick(".pre-image-uploader a.image-cancel.js-cancel"); + + casper.waitForSelector(".entry-preview .js-drop-zone.image-uploader", assertEmptyImageUploaderDisplaysCorrectly); + + // Test image url source location + casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + }); + + casper.then(function () { + casper.writeContentToCodeMirror("![]()"); + }); + + casper.waitForSelector(".entry-preview .js-drop-zone.image-uploader", function onSuccess() { + casper.thenClick(".entry-preview .image-uploader a.image-url"); + }); + + var imageURL = "random.url"; + casper.waitForSelector(".image-uploader-url", function onSuccess() { + casper.sendKeys(".image-uploader-url input.url.js-upload-url", imageURL); + casper.thenClick(".js-button-accept.button-save"); + }); + + casper.waitForSelector(".entry-preview .js-drop-zone.pre-image-uploader", function onSuccess() { + var imageJQuerySelector = ".entry-preview img.js-upload-target[src='" + imageURL + "']" + test.assertExists(imageJQuerySelector, "Uploaded image tag properly links to inputted image URL"); + }); + +}); + CasperTest.begin('Required Title', 4, function suite(test) { casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() { test.assertTitle("Ghost Admin", "Ghost admin has no title"); @@ -131,6 +206,117 @@ CasperTest.begin('Title Trimming', 2, function suite(test) { }); }); +CasperTest.begin("Tag editor", 6, function suite(test) { + casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + }); + + var tagName = "someTagName"; + + casper.then(function () { + test.assertExists("#entry-tags", "should have tag label area"); + test.assertExists("#entry-tags .tag-label", "should have tag label icon"); + test.assertExists("#entry-tags input.tag-input", "should have tag input area"); + casper.sendKeys("#entry-tags input.tag-input", tagName); + casper.sendKeys("#entry-tags input.tag-input", casper.page.event.key.Enter); + }); + + var createdTagSelector = "#entry-tags .tags .tag"; + casper.waitForSelector(createdTagSelector, function onSuccess() { + test.assertSelectorHasText(createdTagSelector, tagName, "typing enter after tag name should create tag"); + }); + + casper.thenClick(createdTagSelector); + + casper.then(function () { + test.assertDoesntExist(createdTagSelector, "clicking the tag should delete the tag"); + }); + +}); + +CasperTest.begin("Post settings menu", 17, function suite(test) { + casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + }); + + casper.then(function () { + test.assertExists("#publish-bar a.post-settings", "icon toggle should exist"); + test.assertNotVisible("#publish-bar .post-settings-menu", "popup menu should not be visible at startup"); + test.assertExists(".post-settings-menu input#url", "url field exists"); + test.assertExists(".post-settings-menu input#pub-date", "publication date field exists"); + test.assertExists(".post-settings-menu input#static-page", "static page checkbox field exists"); + test.assertExists(".post-settings-menu a.delete", "delete post button exists") + }); + + casper.thenClick("#publish-bar a.post-settings"); + + casper.waitUntilVisible("#publish-bar .post-settings-menu", function onSuccess() { + test.assert(true, "popup menu should be visible after clicking post-settings icon"); + test.assertNotVisible(".post-settings-menu a.delete", "delete post btn shouldn't be visible on unsaved drafts"); + }); + + casper.thenClick("#publish-bar a.post-settings"); + + casper.waitWhileVisible("#publish-bar .post-settings-menu", function onSuccess() { + test.assert(true, "popup menu should not be visible after clicking post-settings icon"); + }); + + // Enter a title and save draft so converting to/from static post + // will result in notifications and 'Delete This Post' button appears + casper.then(function (){ + casper.sendKeys("#entry-title", "aTitle"); + casper.thenClick(".js-publish-button"); + }); + + casper.thenClick("#publish-bar a.post-settings"); + + casper.waitUntilVisible("#publish-bar .post-settings-menu", function onSuccess() { + test.assertVisible(".post-settings-menu a.delete", "delete post button should be visible for saved drafts"); + }); + + // Test Static Page conversion + casper.thenClick(".post-settings-menu #static-page"); + + var staticPageConversionText = "Successfully converted to static page."; + casper.waitForText(staticPageConversionText, function onSuccess() { + test.assertSelectorHasText( + ".notification-success", staticPageConversionText, "correct static page conversion notification appears"); + }) + + casper.thenClick(".post-settings-menu #static-page"); + + var postConversionText = "Successfully converted to post."; + casper.waitForText(postConversionText, function onSuccess() { + test.assertSelectorHasText( + ".notification-success", postConversionText, "correct post conversion notification appears"); + }); + + // Test Delete Post Modal + casper.thenClick(".post-settings-menu a.delete"); + + casper.waitUntilVisible("#modal-container", function onSuccess() { + test.assert(true, "delete post modal is visible after clicking delete"); + test.assertSelectorHasText( + "#modal-container .modal-header", + "Are you sure you want to delete this post?", + "delete post modal header has correct text"); + }); + + casper.thenClick("#modal-container .js-button-reject"); + + casper.waitWhileVisible("#modal-container", function onSuccess() { + test.assert(true, "clicking cancel should close the delete post modal"); + }); + + casper.thenClick("#publish-bar a.post-settings"); + casper.thenClick(".post-settings-menu a.delete"); + casper.thenClick("#modal-container .js-button-accept"); + + casper.waitForUrl(/ghost\/content\/$/, function onSuccess() { + test.assert(true, "clicking the delete post button should bring us to the content page"); + }); +}); + CasperTest.begin('Publish menu - new post', 10, function suite(test) { casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() { test.assertTitle("Ghost Admin", 'Ghost admin has no title'); From 18be7a8999af1d2ea1a37d5ba108b959546dcca5 Mon Sep 17 00:00:00 2001 From: John O'Nolan Date: Sat, 15 Mar 2014 17:12:49 +0100 Subject: [PATCH 14/16] Get rid of old modal types Fixes TryGhost/Ghost-UI/issues/17 --- core/client/views/editor.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/client/views/editor.js b/core/client/views/editor.js index aeb26f58739..35b29605c65 100644 --- a/core/client/views/editor.js +++ b/core/client/views/editor.js @@ -387,7 +387,6 @@ model: { options: { close: true, - type: "info", style: ["wide"], animation: 'fade' }, @@ -531,7 +530,6 @@ model: { options: { close: true, - type: "info", style: ["wide"], animation: 'fade' }, From c612ca21361e71eb19b62a100d80815a6d7f9c76 Mon Sep 17 00:00:00 2001 From: Fabian Becker Date: Sat, 15 Mar 2014 17:47:33 +0100 Subject: [PATCH 15/16] Watch changes in Ghost-UI refs TryGhost/Ghost-UI#18 - Add bower_components/ghost-ui to grunt watch --- Gruntfile.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Gruntfile.js b/Gruntfile.js index 7c775754ed7..b10942c1b60 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -67,6 +67,8 @@ var path = require('path'), files: [ // Theme CSS 'content/themes/casper/css/*.css', + // Ghost UI CSS + 'bower_components/ghost-ui/dist/css/*.css', // Theme JS 'content/themes/casper/js/*.js', // Admin JS From deb6da5ed6edaf3526f13ade9fd6f5be896a87b1 Mon Sep 17 00:00:00 2001 From: John O'Nolan Date: Sun, 16 Mar 2014 09:06:56 +0100 Subject: [PATCH 16/16] Remove .bowerrc If there are no further comments or suggestions for #2386 then I think it would be good to get this in. Remove .bowerrc from base repo but allows it to still be used for people with specialised dev environments by adding to .gitignore --- .bowerrc | 3 --- .gitignore | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 .bowerrc diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index df4bceeceb9..00000000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "bower_components" -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3b5b4c59af3..b0e08e1b995 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ results npm-debug.log node_modules bower_components - +.bowerrc .idea/* *.iml projectFilesBackup