From 2d783ac3d480968075800223576d705230955dc8 Mon Sep 17 00:00:00 2001 From: David Arvelo Date: Wed, 7 May 2014 01:28:51 -0400 Subject: [PATCH] DB API returns JSON-API compatible objects. Export triggers 'Save As' dialog. closes #2647 - GET method returns { db: [exportedData] } - POST, DELETE methods return { db: [] } - 'delete all content' test updated - Attach 'Content-Disposition' header on DB export for 'Save As' browser dialog - Add DB API functional test for Export --- core/server/api/db.js | 10 ++- core/server/api/index.js | 29 ++++++- core/test/functional/routes/api/db_test.js | 98 ++++++++++++++++++++++ core/test/integration/api/api_db_spec.js | 5 +- 4 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 core/test/functional/routes/api/db_test.js diff --git a/core/server/api/db.js b/core/server/api/db.js index fa48c2abd0..8217f3c3ed 100644 --- a/core/server/api/db.js +++ b/core/server/api/db.js @@ -20,7 +20,9 @@ db = { // Export data, otherwise send error 500 return canThis(self.user).exportContent.db().then(function () { - return dataExport().otherwise(function (error) { + return dataExport().then(function (exportedData) { + return when.resolve({ db: [exportedData] }); + }).otherwise(function (error) { return when.reject({type: 'InternalServerError', message: error.message || error}); }); }, function () { @@ -46,7 +48,7 @@ db = { return api.settings.read.call({ internal: true }, { key: 'databaseVersion' }).then(function (response) { var setting = response.settings[0]; - + return when(setting.value); }, function () { return when('002'); @@ -90,7 +92,7 @@ db = { }).then(function importSuccess() { return api.settings.updateSettingsCache(); }).then(function () { - return when.resolve({message: 'Posts, tags and other data successfully imported'}); + return when.resolve({ db: [] }); }).otherwise(function importFailure(error) { return when.reject({type: 'InternalServerError', message: error.message || error}); }).finally(function () { @@ -107,7 +109,7 @@ db = { return canThis(self.user).deleteAllContent.db().then(function () { return when(dataProvider.deleteAllContent()) .then(function () { - return when.resolve({message: 'Successfully deleted all content from your blog.'}); + return when.resolve({ db: [] }); }, function (error) { return when.reject({message: error.message || error}); }); diff --git a/core/server/api/index.js b/core/server/api/index.js index 5d63687c28..1e6a093761 100644 --- a/core/server/api/index.js +++ b/core/server/api/index.js @@ -109,6 +109,22 @@ function locationHeader(req, result) { return when(location); } +// create a header that invokes the 'Save As' dialog +// in the browser when exporting the database to file. +// The 'filename' parameter is governed by [RFC6266](http://tools.ietf.org/html/rfc6266#section-4.3). +// +// for encoding whitespace and non-ISO-8859-1 characters, you MUST +// use the "filename*=" attribute, NOT "filename=". Ideally, both. +// see: http://tools.ietf.org/html/rfc598 +// examples: http://tools.ietf.org/html/rfc6266#section-5 +// +// we'll use ISO-8859-1 characters here to keep it simple. +function dbExportSaveAsHeader() { + // replace ':' with '_' for OS that don't support it + var now = (new Date()).toJSON().replace(/:/g, '_'); + return 'Attachment; filename="ghost-' + now + '.json"'; +} + // ### requestHandler // decorator for api functions which are called via an HTTP request // takes the API method and wraps it so that it gets data from the request and returns a sensible JSON response @@ -127,6 +143,13 @@ requestHandler = function (apiMethod) { }); } }) + .then(function () { + if (apiMethod === db.exportContent) { + res.set({ + "Content-Disposition": dbExportSaveAsHeader() + }); + } + }) .then(function () { return locationHeader(req, result).then(function (header) { if (header) { @@ -134,7 +157,7 @@ requestHandler = function (apiMethod) { 'Location': header }); } - + res.json(result || {}); }); }); @@ -148,10 +171,10 @@ requestHandler = function (apiMethod) { _.each(error, function (erroritem) { var errorContent = {}; - + //TODO: add logic to set the correct status code errorCode = errorTypes[erroritem.type].code || 500; - + errorContent['message'] = _.isString(erroritem) ? erroritem : (_.isObject(erroritem) ? erroritem.message : 'Unknown API Error'); errorContent['type'] = erroritem.type || 'InternalServerError'; errors.push(errorContent); diff --git a/core/test/functional/routes/api/db_test.js b/core/test/functional/routes/api/db_test.js new file mode 100644 index 0000000000..569b77a0b3 --- /dev/null +++ b/core/test/functional/routes/api/db_test.js @@ -0,0 +1,98 @@ +/*global describe, it, before, after */ +var supertest = require('supertest'), + express = require('express'), + should = require('should'), + testUtils = require('../../../utils'), + + ghost = require('../../../../../core'), + + httpServer, + request; + + +describe('DB API', function () { + var user = testUtils.DataGenerator.forModel.users[0], + csrfToken = ''; + + before(function (done) { + var app = express(); + + ghost({app: app}).then(function (_httpServer) { + httpServer = _httpServer; + // request = supertest(app); + request = supertest.agent(app); + + testUtils.clearData() + .then(function () { + return testUtils.initData(); + }) + .then(function () { + return testUtils.insertDefaultFixtures(); + }) + .then(function () { + + request.get('/ghost/signin/') + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + var pattern_meta = //i; + pattern_meta.should.exist; + csrfToken = res.text.match(pattern_meta)[1]; + + process.nextTick(function() { + request.post('/ghost/signin/') + .set('X-CSRF-Token', csrfToken) + .send({email: user.email, password: user.password}) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + request.saveCookies(res); + request.get('/ghost/') + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + csrfToken = res.text.match(pattern_meta)[1]; + done(); + }); + }); + + }); + + }); + }).catch(done); + }).catch(function (e) { + console.log('Ghost Error: ', e); + console.log(e.stack); + }); + }); + + after(function () { + httpServer.close(); + }); + + it('attaches the Content-Disposition header on export', function (done) { + request.get(testUtils.API.getApiQuery('db/')) + .expect(200) + .expect('Content-Disposition', /Attachment; filename="[A-Za-z0-9._-]+\.json"/) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + res.should.be.json; + var jsonResponse = res.body; + should.exist(jsonResponse.db); + jsonResponse.db.should.have.length(1); + done(); + }); + }); +}); diff --git a/core/test/integration/api/api_db_spec.js b/core/test/integration/api/api_db_spec.js index 30f345550a..467d0b0df3 100644 --- a/core/test/integration/api/api_db_spec.js +++ b/core/test/integration/api/api_db_spec.js @@ -39,8 +39,9 @@ describe('DB API', function () { permissions.init().then(function () { return dbAPI.deleteAllContent.call({user: 1}); }).then(function (result) { - should.exist(result.message); - result.message.should.equal('Successfully deleted all content from your blog.'); + should.exist(result.db); + result.db.should.be.instanceof(Array); + result.db.should.be.empty; }).then(function () { TagsAPI.browse().then(function (results) { should.exist(results);