diff --git a/core/server/data/default-settings.json b/core/server/data/default-settings.json index 2365f796da..0edfebf39b 100644 --- a/core/server/data/default-settings.json +++ b/core/server/data/default-settings.json @@ -1,7 +1,7 @@ { "core": { "databaseVersion": { - "defaultValue": "001" + "defaultValue": "002" }, "dbHash": { "defaultValue": null diff --git a/core/server/data/import/000.js b/core/server/data/import/000.js index b6ea5e4a55..9f134e0e3d 100644 --- a/core/server/data/import/000.js +++ b/core/server/data/import/000.js @@ -12,7 +12,8 @@ Importer000 = function () { this.importFrom = { '000': this.basicImport, - '001': this.basicImport + '001': this.basicImport, + '002': this.basicImport }; }; diff --git a/core/server/data/import/002.js b/core/server/data/import/002.js new file mode 100644 index 0000000000..84ebbc71b6 --- /dev/null +++ b/core/server/data/import/002.js @@ -0,0 +1,8 @@ +var Importer000 = require('./000'); + +module.exports = { + Importer002: Importer000, + importData: function (data) { + return new Importer000.importData(data); + } +}; \ No newline at end of file diff --git a/core/server/data/migration/index.js b/core/server/data/migration/index.js index a2a69ca7c8..60260191f4 100644 --- a/core/server/data/migration/index.js +++ b/core/server/data/migration/index.js @@ -77,11 +77,15 @@ function createTable(table) { var column, columnKeys = _.keys(schema[table]); _.each(columnKeys, function (key) { - if (schema[table][key].hasOwnProperty('maxlength')) { + // creation distinguishes between text with fieldtype, string with maxlength and all others + if (schema[table][key].type === 'text' && schema[table][key].hasOwnProperty('fieldtype')) { + column = t[schema[table][key].type](key, schema[table][key].fieldtype); + } else if (schema[table][key].type === 'string' && schema[table][key].hasOwnProperty('maxlength')) { column = t[schema[table][key].type](key, schema[table][key].maxlength); } else { column = t[schema[table][key].type](key); } + if (schema[table][key].hasOwnProperty('nullable') && schema[table][key].nullable === true) { column.nullable(); } else { @@ -229,12 +233,37 @@ migrateUpFreshDb = function () { }); }; +// This function changes the type of posts.html and posts.markdown columns to mediumtext. Due to +// a wrong datatype in schema.js some installations using mysql could have been created using the +// data type text instead of mediumtext. +// For details see: https://github.com/TryGhost/Ghost/issues/1947 +function checkMySQLPostTable() { + return knex.raw("SHOW FIELDS FROM posts where Field ='html' OR Field = 'markdown'").then(function (response) { + return _.flatten(_.map(response[0], function (entry) { + if (entry.Type.toLowerCase() !== 'mediumtext') { + return knex.raw("ALTER TABLE posts MODIFY " + entry.Field + " MEDIUMTEXT").then(function () { + return when.resolve(); + }); + } + })); + }); +} + // Migrate from a specific version to the latest migrateUp = function () { return getTables().then(function (oldTables) { + // if tables exist and lient is mysqls check if posts table is okay + if (!_.isEmpty(oldTables) && client === 'mysql') { + return checkMySQLPostTable().then(function () { + return oldTables; + }); + } + return oldTables; + }).then(function (oldTables) { var deleteCommands = getDeleteCommands(oldTables, schemaTables), addCommands = getAddCommands(oldTables, schemaTables), commands = []; + if (!_.isEmpty(deleteCommands)) { commands = commands.concat(deleteCommands); } diff --git a/core/server/data/schema.js b/core/server/data/schema.js index bf90ee90ff..fec7efb26a 100644 --- a/core/server/data/schema.js +++ b/core/server/data/schema.js @@ -4,8 +4,8 @@ var db = { uuid: {type: 'string', maxlength: 36, nullable: false}, title: {type: 'string', maxlength: 150, nullable: false}, slug: {type: 'string', maxlength: 150, nullable: false, unique: true}, - markdown: {type: 'text', maxlength: 16777215, nullable: true}, - html: {type: 'text', maxlength: 16777215, nullable: true}, + markdown: {type: 'text', maxlength: 16777215, fieldtype: 'medium', nullable: true}, + html: {type: 'text', maxlength: 16777215, fieldtype: 'medium', nullable: true}, image: {type: 'text', maxlength: 2000, nullable: true}, featured: {type: 'bool', nullable: false, defaultTo: false}, page: {type: 'bool', nullable: false, defaultTo: false}, diff --git a/core/test/unit/export_spec.js b/core/test/unit/export_spec.js index f0e729fa8b..793662ce8b 100644 --- a/core/test/unit/export_spec.js +++ b/core/test/unit/export_spec.js @@ -36,7 +36,7 @@ describe("Exporter", function () { it("exports data", function (done) { // Stub migrations to return 000 as the current database version var migrationStub = sinon.stub(migration, "getDatabaseVersion", function () { - return when.resolve("001"); + return when.resolve("002"); }); exporter().then(function (exportData) { @@ -48,8 +48,8 @@ describe("Exporter", function () { should.exist(exportData.meta); should.exist(exportData.data); - exportData.meta.version.should.equal("001"); - _.findWhere(exportData.data.settings, {key: "databaseVersion"}).value.should.equal("001"); + exportData.meta.version.should.equal("002"); + _.findWhere(exportData.data.settings, {key: "databaseVersion"}).value.should.equal("002"); _.each(tables, function (name) { should.exist(exportData.data[name]); diff --git a/core/test/unit/import_spec.js b/core/test/unit/import_spec.js index f1fa41e560..7423aecb95 100644 --- a/core/test/unit/import_spec.js +++ b/core/test/unit/import_spec.js @@ -14,6 +14,7 @@ var testUtils = require('../utils'), importer = require('../../server/data/import'), Importer000 = require('../../server/data/import/000'), Importer001 = require('../../server/data/import/001'), + Importer002 = require('../../server/data/import/002'), fixtures = require('../../server/data/fixtures'), Settings = require('../../server/models/settings').Settings; @@ -59,6 +60,21 @@ describe("Import", function () { }).then(null, done); }); + it("resolves 002", function (done) { + var importStub = sinon.stub(Importer002, "importData", function () { + return when.resolve(); + }), + fakeData = { test: true }; + + importer("002", fakeData).then(function () { + importStub.calledWith(fakeData).should.equal(true); + + importStub.restore(); + + done(); + }).then(null, done); + }); + describe("000", function () { should.exist(Importer000); @@ -113,7 +129,7 @@ describe("Import", function () { // test settings settings.length.should.be.above(0, 'Wrong number of settings'); - _.findWhere(settings, {key: "databaseVersion"}).value.should.equal("001", 'Wrong database version'); + _.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version'); // test tags tags.length.should.equal(exportData.data.tags.length, 'no new tags'); @@ -191,7 +207,7 @@ describe("Import", function () { // test settings settings.length.should.be.above(0, 'Wrong number of settings'); - _.findWhere(settings, {key: "databaseVersion"}).value.should.equal("001", 'Wrong database version'); + _.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version'); // activeTheme should NOT have been overridden _.findWhere(settings, {key: "activeTheme"}).value.should.equal("casper", 'Wrong theme'); @@ -254,7 +270,7 @@ describe("Import", function () { // test settings settings.length.should.be.above(0, 'Wrong number of settings'); - _.findWhere(settings, {key: "databaseVersion"}).value.should.equal("001", 'Wrong database version'); + _.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version'); // test tags tags.length.should.equal(exportData.data.tags.length, 'no new tags'); @@ -300,7 +316,7 @@ describe("Import", function () { // test settings settings.length.should.be.above(0, 'Wrong number of settings'); - _.findWhere(settings, {key: "databaseVersion"}).value.should.equal("001", 'Wrong database version'); + _.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version'); // test tags tags.length.should.equal(exportData.data.tags.length, 'no new tags'); @@ -312,4 +328,190 @@ describe("Import", function () { }); }); + describe("002", function () { + should.exist(Importer001); + + beforeEach(function (done) { + // migrate to current version + migration.migrateUp().then(function () { + // Load the fixtures + return fixtures.populateFixtures(); + }).then(function () { + // Initialise the default settings + return Settings.populateDefaults(); + }).then(function () { + return testUtils.insertDefaultUser(); + }).then(function () { + done(); + }).then(null, done); + }); + + it("safely imports data from 002", function (done) { + var exportData, + timestamp = 1349928000000; + + testUtils.loadExportFixture('export-002').then(function (exported) { + exportData = exported; + + // Modify timestamp data for testing + exportData.data.posts[0].created_at = timestamp; + exportData.data.posts[0].updated_at = timestamp; + exportData.data.posts[0].published_at = timestamp; + + return importer("002", exportData); + }).then(function () { + // Grab the data from tables + return when.all([ + knex("users").select(), + knex("posts").select(), + knex("settings").select(), + knex("tags").select() + ]); + }).then(function (importedData) { + should.exist(importedData); + + importedData.length.should.equal(4, 'Did not get data successfully'); + + var users = importedData[0], + posts = importedData[1], + settings = importedData[2], + tags = importedData[3], + exportEmail; + + // we always have 1 user, the default user we added + users.length.should.equal(1, 'There should only be one user'); + + // user should still have the credentials from the original insert, not the import + users[0].email.should.equal(testUtils.DataGenerator.Content.users[0].email); + users[0].password.should.equal(testUtils.DataGenerator.Content.users[0].password); + // but the name, slug, and bio should have been overridden + users[0].name.should.equal(exportData.data.users[0].name); + users[0].slug.should.equal(exportData.data.users[0].slug); + users[0].bio.should.equal(exportData.data.users[0].bio); + + // import no longer requires all data to be dropped, and adds posts + posts.length.should.equal(exportData.data.posts.length + 1, 'Wrong number of posts'); + + // test settings + settings.length.should.be.above(0, 'Wrong number of settings'); + _.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version'); + + // activeTheme should NOT have been overridden + _.findWhere(settings, {key: "activeTheme"}).value.should.equal("casper", 'Wrong theme'); + + // email address should have been overridden + exportEmail = _.findWhere(exportData.data.settings, {key: "email"}).value; + _.findWhere(settings, {key: "email"}).value.should.equal(exportEmail, 'Wrong email in settings'); + + // test tags + tags.length.should.equal(exportData.data.tags.length, 'no new tags'); + + // Ensure imported post retains set timestamp + // When in sqlite we are returned a unix timestamp number, + // in MySQL we're returned a date object. + // We pass the returned post always through the date object + // to ensure the return is consistant for all DBs. + assert.equal(new Date(posts[1].created_at).getTime(), timestamp); + assert.equal(new Date(posts[1].updated_at).getTime(), timestamp); + assert.equal(new Date(posts[1].published_at).getTime(), timestamp); + + done(); + }).then(null, done); + }); + + it("doesn't import invalid post data from 002", function (done) { + var exportData; + + + testUtils.loadExportFixture('export-002').then(function (exported) { + exportData = exported; + + //change title to 151 characters + exportData.data.posts[0].title = new Array(152).join('a'); + exportData.data.posts[0].tags = 'Tag'; + return importer("002", exportData); + }).then(function () { + (1).should.eql(0, 'Data import should not resolve promise.'); + }, function (error) { + error.should.eql('Error importing data: Post title maximum length is 150 characters.'); + + when.all([ + knex("users").select(), + knex("posts").select(), + knex("settings").select(), + knex("tags").select() + ]).then(function (importedData) { + should.exist(importedData); + + importedData.length.should.equal(4, 'Did not get data successfully'); + + var users = importedData[0], + posts = importedData[1], + settings = importedData[2], + tags = importedData[3]; + + // we always have 1 user, the default user we added + users.length.should.equal(1, 'There should only be one user'); + // import no longer requires all data to be dropped, and adds posts + posts.length.should.equal(exportData.data.posts.length, 'Wrong number of posts'); + + // test settings + settings.length.should.be.above(0, 'Wrong number of settings'); + _.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version'); + + // test tags + tags.length.should.equal(exportData.data.tags.length, 'no new tags'); + + done(); + }); + + }).then(null, done); + }); + + it("doesn't import invalid settings data from 002", function (done) { + var exportData; + + testUtils.loadExportFixture('export-002').then(function (exported) { + exportData = exported; + //change to blank settings key + exportData.data.settings[3].key = null; + return importer("002", exportData); + }).then(function () { + (1).should.eql(0, 'Data import should not resolve promise.'); + }, function (error) { + error.should.eql('Error importing data: Setting key cannot be blank'); + + when.all([ + knex("users").select(), + knex("posts").select(), + knex("settings").select(), + knex("tags").select() + ]).then(function (importedData) { + should.exist(importedData); + + importedData.length.should.equal(4, 'Did not get data successfully'); + + var users = importedData[0], + posts = importedData[1], + settings = importedData[2], + tags = importedData[3]; + + // we always have 1 user, the default user we added + users.length.should.equal(1, 'There should only be one user'); + // import no longer requires all data to be dropped, and adds posts + posts.length.should.equal(exportData.data.posts.length, 'Wrong number of posts'); + + // test settings + settings.length.should.be.above(0, 'Wrong number of settings'); + _.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version'); + + // test tags + tags.length.should.equal(exportData.data.tags.length, 'no new tags'); + + done(); + }); + + }).then(null, done); + }); + }); }); diff --git a/core/test/utils/fixtures/export-002.json b/core/test/utils/fixtures/export-002.json new file mode 100644 index 0000000000..e0181d4cb0 --- /dev/null +++ b/core/test/utils/fixtures/export-002.json @@ -0,0 +1,331 @@ +{ + "meta": { + "exported_on": 1388318311015, + "version": "002" + }, + "data": { + "posts": [ + { + "id": 1, + "uuid": "8492fbba-1102-4b53-8e3e-abe207952f0c", + "title": "Welcome to Ghost", + "slug": "welcome-to-ghost", + "markdown": "You're live! Nice.", + "html": "
You're live! Nice.
", + "image": null, + "featured": 0, + "page": 0, + "status": "published", + "language": "en_US", + "meta_title": null, + "meta_description": null, + "author_id": 1, + "created_at": 1388318310782, + "created_by": 1, + "updated_at": 1388318310782, + "updated_by": 1, + "published_at": 1388318310783, + "published_by": 1 + } + ], + "users": [ + { + "id": 1, + "uuid": "e5188224-4742-4c32-a2d6-e9c5c5d4c123", + "name": "Josephine Bloggs", + "slug": "josephine-blogs", + "password": "$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKABC", + "email": "josephinebloggs@example.com", + "image": null, + "cover": null, + "bio": "A blogger", + "website": null, + "location": null, + "accessibility": null, + "status": "active", + "language": "en_US", + "meta_title": null, + "meta_description": null, + "last_login": null, + "created_at": 1388319501897, + "created_by": 1, + "updated_at": null, + "updated_by": null + } + ], + "roles": [ + { + "id": 1, + "uuid": "d2ea9c7f-7e6b-4cae-b009-35c298206852", + "name": "Administrator", + "description": "Administrators", + "created_at": 1388318310794, + "created_by": 1, + "updated_at": 1388318310794, + "updated_by": 1 + }, + { + "id": 2, + "uuid": "b0d7d6b0-5b88-45b5-b0e5-a487741b843d", + "name": "Editor", + "description": "Editors", + "created_at": 1388318310796, + "created_by": 1, + "updated_at": 1388318310796, + "updated_by": 1 + }, + { + "id": 3, + "uuid": "9f72e817-5490-4ccf-bc78-c557dc9613ca", + "name": "Author", + "description": "Authors", + "created_at": 1388318310799, + "created_by": 1, + "updated_at": 1388318310799, + "updated_by": 1 + } + ], + "roles_users": [ + { + "id": 1, + "role_id": 1, + "user_id": 1 + } + ], + "permissions": [ + { + "id": 1, + "uuid": "bdfbd261-e0fb-4c8e-abab-aece7a9e8e34", + "name": "Edit posts", + "object_type": "post", + "action_type": "edit", + "object_id": null, + "created_at": 1388318310803, + "created_by": 1, + "updated_at": 1388318310803, + "updated_by": 1 + }, + { + "id": 2, + "uuid": "580d31c4-e3db-40f3-969d-9a1caea9d1bb", + "name": "Remove posts", + "object_type": "post", + "action_type": "remove", + "object_id": null, + "created_at": 1388318310814, + "created_by": 1, + "updated_at": 1388318310814, + "updated_by": 1 + }, + { + "id": 3, + "uuid": "c1f8b024-e383-494a-835d-6fb673f143db", + "name": "Create posts", + "object_type": "post", + "action_type": "create", + "object_id": null, + "created_at": 1388318310818, + "created_by": 1, + "updated_at": 1388318310818, + "updated_by": 1 + } + ], + "permissions_users": [], + "permissions_roles": [ + { + "id": 1, + "role_id": 1, + "permission_id": 1 + }, + { + "id": 2, + "role_id": 1, + "permission_id": 2 + }, + { + "id": 3, + "role_id": 1, + "permission_id": 3 + } + ], + "settings": [ + { + "id": 1, + "uuid": "f90aa810-4fa2-49fe-a39b-7c0d2ebb473e", + "key": "databaseVersion", + "value": "001", + "type": "core", + "created_at": 1388318310829, + "created_by": 1, + "updated_at": 1388318310829, + "updated_by": 1 + }, + { + "id": 2, + "uuid": "95ce1c53-69b0-4f5f-be91-d3aeb39046b5", + "key": "dbHash", + "value": null, + "type": "core", + "created_at": 1388318310829, + "created_by": 1, + "updated_at": 1388318310829, + "updated_by": 1 + }, + { + "id": 3, + "uuid": "c356fbde-0bc5-4fe1-9309-2510291aa34d", + "key": "title", + "value": "Ghost", + "type": "blog", + "created_at": 1388318310830, + "created_by": 1, + "updated_at": 1388318310830, + "updated_by": 1 + }, + { + "id": 4, + "uuid": "858dc11f-8f9e-4011-99ee-d94c48d5a2ce", + "key": "description", + "value": "Just a blogging platform.", + "type": "blog", + "created_at": 1388318310830, + "created_by": 1, + "updated_at": 1388318310830, + "updated_by": 1 + }, + { + "id": 5, + "uuid": "37ca5ae7-bca6-4dd5-8021-4ef6c6dcb097", + "key": "email", + "value": "josephinebloggs@example.com", + "type": "blog", + "created_at": 1388318310830, + "created_by": 1, + "updated_at": 1388318310830, + "updated_by": 1 + }, + { + "id": 6, + "uuid": "1672d62c-fab7-4f22-b333-8cf760189f67", + "key": "logo", + "value": "", + "type": "blog", + "created_at": 1388318310830, + "created_by": 1, + "updated_at": 1388318310830, + "updated_by": 1 + }, + { + "id": 7, + "uuid": "cd8b0456-578b-467a-857e-551bad17a14d", + "key": "cover", + "value": "", + "type": "blog", + "created_at": 1388318310830, + "created_by": 1, + "updated_at": 1388318310830, + "updated_by": 1 + }, + { + "id": 8, + "uuid": "c4a074a4-05c7-49f7-83eb-068302c15d82", + "key": "defaultLang", + "value": "en_US", + "type": "blog", + "created_at": 1388318310830, + "created_by": 1, + "updated_at": 1388318310830, + "updated_by": 1 + }, + { + "id": 9, + "uuid": "21f2f5da-9bee-4dae-b3b7-b8d7baf8be33", + "key": "postsPerPage", + "value": "6", + "type": "blog", + "created_at": 1388318310830, + "created_by": 1, + "updated_at": 1388318310830, + "updated_by": 1 + }, + { + "id": 10, + "uuid": "2d21b736-f85a-4119-a0e3-5fc898b1bf47", + "key": "forceI18n", + "value": "true", + "type": "blog", + "created_at": 1388318310831, + "created_by": 1, + "updated_at": 1388318310831, + "updated_by": 1 + }, + { + "id": 11, + "uuid": "5c5b91b8-6062-4104-b855-9e121f72b0f0", + "key": "permalinks", + "value": "/:slug/", + "type": "blog", + "created_at": 1388318310831, + "created_by": 1, + "updated_at": 1388318310831, + "updated_by": 1 + }, + { + "id": 12, + "uuid": "795cb328-3e38-4906-81a8-fcdff19d914f", + "key": "activeTheme", + "value": "notcasper", + "type": "theme", + "created_at": 1388318310831, + "created_by": 1, + "updated_at": 1388318310831, + "updated_by": 1 + }, + { + "id": 13, + "uuid": "f3afce35-5166-453e-86c3-50dfff74dca7", + "key": "activePlugins", + "value": "[]", + "type": "plugin", + "created_at": 1388318310831, + "created_by": 1, + "updated_at": 1388318310831, + "updated_by": 1 + }, + { + "id": 14, + "uuid": "2ea560a3-2304-449d-a62b-f7b622987510", + "key": "installedPlugins", + "value": "[]", + "type": "plugin", + "created_at": 1388318310831, + "created_by": 1, + "updated_at": 1388318310831, + "updated_by": 1 + } + ], + "tags": [ + { + "id": 1, + "uuid": "a950117a-9735-4584-931d-25a28015a80d", + "name": "Getting Started", + "slug": "getting-started", + "description": null, + "parent_id": null, + "meta_title": null, + "meta_description": null, + "created_at": 1388318310790, + "created_by": 1, + "updated_at": 1388318310790, + "updated_by": 1 + } + ], + "posts_tags": [ + { + "id": 1, + "post_id": 1, + "tag_id": 1 + } + ] + } +} \ No newline at end of file