├── core ├── client │ ├── tpl │ │ ├── modals │ │ │ ├── blank.hbs │ │ │ ├── copyToHTML.hbs │ │ │ ├── uploadImage.hbs │ │ │ └── markdown.hbs │ │ ├── notification.hbs │ │ ├── settings │ │ │ ├── sidebar.hbs │ │ │ ├── general.hbs │ │ │ └── user-profile.hbs │ │ ├── forgotten.hbs │ │ ├── list-item.hbs │ │ ├── signup.hbs │ │ ├── login.hbs │ │ ├── modal.hbs │ │ └── preview.hbs │ ├── assets │ │ ├── sass │ │ │ ├── ie.scss │ │ │ ├── modules │ │ │ │ └── animations.scss │ │ │ ├── screen.scss │ │ │ └── layouts │ │ │ │ ├── errors.scss │ │ │ │ └── plugins.scss │ │ ├── img │ │ │ ├── large.png │ │ │ ├── medium.png │ │ │ ├── small.png │ │ │ ├── 404-ghost.png │ │ │ ├── loadingcat.gif │ │ │ ├── 404-ghost@2x.png │ │ │ ├── touch-icon-ipad.png │ │ │ └── touch-icon-iphone.png │ │ ├── fonts │ │ │ ├── icons.eot │ │ │ ├── icons.ttf │ │ │ └── icons.woff │ │ └── vendor │ │ │ ├── to-title-case.js │ │ │ ├── showdown │ │ │ └── extensions │ │ │ │ └── ghostdown.js │ │ │ ├── codemirror │ │ │ ├── addon │ │ │ │ └── mode │ │ │ │ │ └── overlay.js │ │ │ └── mode │ │ │ │ └── gfm │ │ │ │ ├── index.html │ │ │ │ ├── gfm.js │ │ │ │ └── test.js │ │ │ └── icheck │ │ │ └── jquery.icheck.min.js │ ├── models │ │ ├── tag.js │ │ ├── themes.js │ │ ├── settings.js │ │ ├── user.js │ │ ├── base.js │ │ ├── uploadModal.js │ │ ├── widget.js │ │ └── post.js │ ├── helpers │ │ └── index.js │ ├── views │ │ ├── debug.js │ │ └── login.js │ ├── init.js │ ├── toggle.js │ ├── mobile-interactions.js │ └── router.js ├── test │ ├── unit │ │ ├── fixtures │ │ │ ├── test.hbs │ │ │ └── theme │ │ │ │ └── partials │ │ │ │ └── test.hbs │ │ ├── api_posts_spec.js │ │ ├── plugins_spec.js │ │ ├── export_spec.js │ │ ├── testUtils.js │ │ ├── client_ghostdown_spec.js │ │ ├── middleware_spec.js │ │ ├── model_roles_spec.js │ │ ├── utils │ │ │ └── api.js │ │ ├── model_permissions_spec.js │ │ ├── errorHandling_spec.js │ │ ├── import_spec.js │ │ ├── model_users_spec.js │ │ └── mail_spec.js │ └── functional │ │ ├── frontend │ │ ├── route_test.js │ │ └── rss_test.js │ │ └── admin │ │ ├── flow_test.js │ │ ├── content_test.js │ │ ├── logout_test.js │ │ └── login_test.js ├── shared │ ├── img │ │ ├── user-cover.png │ │ └── user-image.png │ ├── lang │ │ ├── en_US.json │ │ └── i18n.js │ ├── favicon.ico │ └── vendor │ │ └── showdown │ │ └── extensions │ │ └── github.js ├── server │ ├── views │ │ ├── login.hbs │ │ ├── forgotten.hbs │ │ ├── signup.hbs │ │ ├── settings.hbs │ │ ├── partials │ │ │ ├── notifications.hbs │ │ │ └── navbar.hbs │ │ ├── content.hbs │ │ ├── error.hbs │ │ ├── debug.hbs │ │ ├── user-error.hbs │ │ ├── default.hbs │ │ └── editor.hbs │ ├── helpers │ │ └── tpl │ │ │ ├── nav.hbs │ │ │ └── pagination.hbs │ ├── permissions │ │ └── objectTypeModelMap.js │ ├── data │ │ ├── import │ │ │ ├── index.js │ │ │ └── 000.js │ │ ├── default-settings.json │ │ └── export │ │ │ └── index.js │ ├── models │ │ ├── index.js │ │ ├── role.js │ │ ├── permission.js │ │ ├── tag.js │ │ └── settings.js │ ├── middleware.js │ ├── plugins │ │ ├── GhostPlugin.js │ │ ├── loader.js │ │ └── index.js │ ├── require-tree.js │ ├── mail.js │ └── controllers │ │ └── frontend.js └── config-loader.js ├── content ├── plugins │ └── README.md ├── images │ └── README.md └── data │ └── README.md ├── .gitmodules ├── SECURITY.md ├── .travis.yml ├── index.js ├── .gitignore ├── LICENSE ├── package.json ├── config.example.js └── README.md /core/client/tpl/modals/blank.hbs: -------------------------------------------------------------------------------- 1 | {{content.text}} -------------------------------------------------------------------------------- /core/test/unit/fixtures/test.hbs: -------------------------------------------------------------------------------- 1 |

HelloWorld

-------------------------------------------------------------------------------- /core/client/assets/sass/ie.scss: -------------------------------------------------------------------------------- 1 | /* IE specific override styles. */ -------------------------------------------------------------------------------- /core/test/unit/fixtures/theme/partials/test.hbs: -------------------------------------------------------------------------------- 1 |

HelloWorld Themed

-------------------------------------------------------------------------------- /content/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Content / Plugins 2 | 3 | Coming soon, Ghost plugins will appear here. -------------------------------------------------------------------------------- /core/shared/img/user-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/shared/img/user-cover.png -------------------------------------------------------------------------------- /core/shared/img/user-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/shared/img/user-image.png -------------------------------------------------------------------------------- /core/client/assets/img/large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/client/assets/img/large.png -------------------------------------------------------------------------------- /core/client/assets/img/medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/client/assets/img/medium.png -------------------------------------------------------------------------------- /core/client/assets/img/small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/client/assets/img/small.png -------------------------------------------------------------------------------- /core/server/views/login.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /core/client/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/client/assets/fonts/icons.eot -------------------------------------------------------------------------------- /core/client/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/client/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /core/client/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/client/assets/fonts/icons.woff -------------------------------------------------------------------------------- /core/server/views/forgotten.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
3 | 4 |
-------------------------------------------------------------------------------- /core/server/views/signup.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /core/client/assets/img/404-ghost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/client/assets/img/404-ghost.png -------------------------------------------------------------------------------- /core/client/assets/img/loadingcat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/client/assets/img/loadingcat.gif -------------------------------------------------------------------------------- /core/client/assets/img/404-ghost@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/client/assets/img/404-ghost@2x.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "content/themes/casper"] 2 | path = content/themes/casper 3 | url = git://github.com/TryGhost/Casper.git 4 | -------------------------------------------------------------------------------- /content/images/README.md: -------------------------------------------------------------------------------- 1 | # Content / Images 2 | 3 | If using the standard file storage, Ghost will upload images to this directory. -------------------------------------------------------------------------------- /core/client/assets/img/touch-icon-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/client/assets/img/touch-icon-ipad.png -------------------------------------------------------------------------------- /core/client/assets/img/touch-icon-iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/Ghost/master/core/client/assets/img/touch-icon-iphone.png -------------------------------------------------------------------------------- /content/data/README.md: -------------------------------------------------------------------------------- 1 | # Content / Data 2 | 3 | This is the home of your Ghost database, do not overwrite this folder or any of the files inside of it. -------------------------------------------------------------------------------- /core/client/tpl/modals/copyToHTML.hbs: -------------------------------------------------------------------------------- 1 | Press Ctrl / Cmd + C to copy the following HTML. 2 |
3 | 
4 | 
-------------------------------------------------------------------------------- /core/client/tpl/notification.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{{message}}} 3 | 4 |
5 | -------------------------------------------------------------------------------- /core/server/helpers/tpl/nav.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/server/views/settings.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
3 | 6 | 7 |
8 |
-------------------------------------------------------------------------------- /core/client/models/tag.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, $, _, Backbone */ 2 | (function () { 3 | 'use strict'; 4 | 5 | Ghost.Collections.Tags = Ghost.TemplateModel.extend({ 6 | url: Ghost.settings.apiRoot + '/tags/' 7 | }); 8 | }()); 9 | -------------------------------------------------------------------------------- /core/client/models/themes.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, $, _, Backbone */ 2 | (function () { 3 | 'use strict'; 4 | 5 | Ghost.Models.Themes = Ghost.TemplateModel.extend({ 6 | url: Ghost.settings.apiRoot + '/themes' 7 | }); 8 | 9 | }()); -------------------------------------------------------------------------------- /core/client/tpl/settings/sidebar.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Settings

3 |
4 | -------------------------------------------------------------------------------- /core/client/tpl/modals/uploadImage.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | If you discover a security issue in Ghost, please report it by sending an email to security[at]ghost[dot]org 4 | 5 | This will allow us to assess the risk, and make a fix available before we add a bug report to the Github repo. 6 | 7 | Thanks for helping make Ghost safe for everyone. -------------------------------------------------------------------------------- /core/client/models/settings.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, $, _, Backbone */ 2 | (function () { 3 | 'use strict'; 4 | //id:0 is used to issue PUT requests 5 | Ghost.Models.Settings = Ghost.TemplateModel.extend({ 6 | url: Ghost.settings.apiRoot + '/settings/?type=blog,theme', 7 | id: '0' 8 | }); 9 | 10 | }()); -------------------------------------------------------------------------------- /core/client/tpl/forgotten.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /core/server/permissions/objectTypeModelMap.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'post': require('../models/post').Post, 3 | 'role': require('../models/role').Role, 4 | 'user': require('../models/user').User, 5 | 'permission': require('../models/permission').Permission, 6 | 'setting': require('../models/settings').Settings 7 | }; 8 | -------------------------------------------------------------------------------- /core/server/helpers/tpl/pagination.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/client/models/user.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, $, _, Backbone */ 2 | (function () { 3 | 'use strict'; 4 | 5 | Ghost.Models.User = Ghost.TemplateModel.extend({ 6 | url: Ghost.settings.apiRoot + '/users/me/' 7 | }); 8 | 9 | // Ghost.Collections.Users = Backbone.Collection.extend({ 10 | // url: Ghost.settings.apiRoot + '/users/' 11 | // }); 12 | 13 | }()); 14 | -------------------------------------------------------------------------------- /core/server/data/import/index.js: -------------------------------------------------------------------------------- 1 | var when = require('when'); 2 | 3 | module.exports = function (version, data) { 4 | var importer; 5 | 6 | try { 7 | importer = require('./' + version); 8 | } catch (ignore) { 9 | // Zero effs given 10 | } 11 | 12 | if (!importer) { 13 | return when.reject("No importer found"); 14 | } 15 | 16 | return importer.importData(data); 17 | }; 18 | -------------------------------------------------------------------------------- /core/test/functional/frontend/route_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests logging out and attempting to sign up 3 | */ 4 | 5 | /*globals casper, __utils__, url, testPost, falseUser, email */ 6 | CasperTest.begin('Redirects page 1 request', 1, function suite(test) { 7 | casper.thenOpen(url + 'page/1/', function then(response) { 8 | test.assertEqual(casper.getCurrentUrl().indexOf('page/'), -1, 'Should be redirected to "/".'); 9 | }); 10 | }, true); -------------------------------------------------------------------------------- /core/server/views/partials/notifications.hbs: -------------------------------------------------------------------------------- 1 | {{#if messages}} 2 | {{#each messages}} 3 |
4 |
5 | {{{message}}} 6 | 7 |
8 |
9 | {{/each}} 10 | {{/if}} 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | git: 6 | submodules: false 7 | before_install: 8 | - gem update --system 9 | - gem install sass bourbon 10 | - npm install -g grunt-cli 11 | - git clone git://github.com/n1k0/casperjs.git ~/casperjs 12 | - cd ~/casperjs 13 | - git checkout tags/1.1-beta1 14 | - export PATH=$PATH:`pwd`/bin 15 | - cd - 16 | before_script: 17 | - phantomjs --version 18 | - casperjs --version 19 | - grunt init -------------------------------------------------------------------------------- /core/client/helpers/index.js: -------------------------------------------------------------------------------- 1 | /*globals Handlebars, moment 2 | */ 3 | (function () { 4 | 'use strict'; 5 | Handlebars.registerHelper('date', function (context, block) { 6 | var f = block.hash.format || 'MMM Do, YYYY', 7 | timeago = block.hash.timeago, 8 | date; 9 | if (timeago) { 10 | date = moment(context).fromNow(); 11 | } else { 12 | date = moment(context).format(f); 13 | } 14 | return date; 15 | }); 16 | }()); 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // # Ghost bootloader 2 | // Orchestrates the loading of Ghost 3 | 4 | var configLoader = require('./core/config-loader.js'), 5 | error = require('./core/server/errorHandling'); 6 | 7 | // If no env is set, default to development 8 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 9 | 10 | configLoader.loadConfig().then(function () { 11 | // The server and its dependencies require a populated config 12 | require('./core/server'); 13 | }).otherwise(error.logAndThrowError); 14 | -------------------------------------------------------------------------------- /core/client/tpl/list-item.hbs: -------------------------------------------------------------------------------- 1 | 2 |

{{{title}}}

3 |
4 | 11 | {{!1,934}} 12 |
13 |
-------------------------------------------------------------------------------- /core/test/functional/frontend/rss_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests if RSS exists and is working 3 | */ 4 | CasperTest.begin('Ensure that RSS is available', 3, function suite(test) { 5 | casper.thenOpen(url + 'rss/', function (response) { 6 | test.assertEqual(response.status, 200, 'Response status should be 200.'); 7 | test.assert(this.getPageContent().indexOf('= 0, 'Feed should contain ') >= 0, 'Feed should contain '); 9 | }); 10 | }, true); -------------------------------------------------------------------------------- /core/client/tpl/signup.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 8 |
9 | 10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /core/server/views/content.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
3 |
4 |
5 |
6 | All Posts 7 |
8 | 9 |
10 |
11 |
    12 |
    13 |
    14 | 15 |
    16 |
    17 |
    -------------------------------------------------------------------------------- /core/client/tpl/login.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 5 |
    6 | 7 |
    8 | 9 |
    10 | Forgotten password?{{! • Register new user}} 11 |
    12 |
    13 | -------------------------------------------------------------------------------- /core/client/views/debug.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, $, _, Backbone, JST */ 2 | (function () { 3 | "use strict"; 4 | 5 | Ghost.Views.Debug = Ghost.View.extend({ 6 | events: { 7 | "click .settings-menu a": "handleMenuClick" 8 | }, 9 | 10 | handleMenuClick: function (ev) { 11 | ev.preventDefault(); 12 | 13 | var $target = $(ev.currentTarget); 14 | 15 | // Hide the current content 16 | this.$(".settings-content").hide(); 17 | 18 | // Show the clicked content 19 | this.$("#debug-" + $target.attr("class")).show(); 20 | 21 | return false; 22 | } 23 | }); 24 | 25 | }()); -------------------------------------------------------------------------------- /core/client/models/base.js: -------------------------------------------------------------------------------- 1 | /*global window, document, setTimeout, Ghost, $, _, Backbone, JST, shortcut, NProgress */ 2 | 3 | (function () { 4 | "use strict"; 5 | NProgress.configure({ showSpinner: false }); 6 | 7 | Ghost.TemplateModel = Backbone.Model.extend({ 8 | 9 | // Adds in a call to start a loading bar 10 | // This is sets up a success function which completes the loading bar 11 | fetch : function (options) { 12 | options = options || {}; 13 | 14 | NProgress.start(); 15 | 16 | options.success = function () { 17 | NProgress.done(); 18 | }; 19 | 20 | return Backbone.Collection.prototype.fetch.call(this, options); 21 | } 22 | }); 23 | }()); -------------------------------------------------------------------------------- /core/server/models/index.js: -------------------------------------------------------------------------------- 1 | var migrations = require('../data/migration'); 2 | 3 | module.exports = { 4 | Post: require('./post').Post, 5 | User: require('./user').User, 6 | Role: require('./role').Role, 7 | Permission: require('./permission').Permission, 8 | Settings: require('./settings').Settings, 9 | Tag: require('./tag').Tag, 10 | init: function () { 11 | return migrations.init(); 12 | }, 13 | reset: function () { 14 | return migrations.reset().then(function () { 15 | return migrations.init(); 16 | }); 17 | }, 18 | isPost: function (jsonData) { 19 | return jsonData.hasOwnProperty('html') && jsonData.hasOwnProperty('markdown') 20 | && jsonData.hasOwnProperty('title') && jsonData.hasOwnProperty('slug'); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /core/client/assets/vendor/to-title-case.js: -------------------------------------------------------------------------------- 1 | /* 2 | * To Title Case 2.0.1 – http://individed.com/code/to-title-case/ 3 | * Copyright © 2008–2012 David Gouch. Licensed under the MIT License. 4 | */ 5 | 6 | String.prototype.toTitleCase = function () { 7 | var smallWords = /^(a|an|and|as|at|but|by|en|for|if|in|of|on|or|the|to|vs?\.?|via)$/i; 8 | 9 | return this.replace(/([^\W_]+[^\s-]*) */g, function (match, p1, index, title) { 10 | if (index > 0 && index + p1.length !== title.length && 11 | p1.search(smallWords) > -1 && title.charAt(index - 2) !== ":" && 12 | title.charAt(index - 1).search(/[^\s-]/) < 0) { 13 | return match.toLowerCase(); 14 | } 15 | 16 | if (p1.substr(1).search(/[A-Z]|\../) > -1) { 17 | return match; 18 | } 19 | 20 | return match.charAt(0).toUpperCase() + match.substr(1); 21 | }); 22 | }; -------------------------------------------------------------------------------- /core/client/assets/sass/modules/animations.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Specific styles for re-usable animations in Ghost admin. 3 | * 4 | * Table of Contents: 5 | * 6 | * 7 | */ 8 | 9 | 10 | /* ============================================================================= 11 | General 12 | ============================================================================= */ 13 | 14 | @-webkit-keyframes off-canvas { 15 | 0% { left:0; } 16 | 100% { left:300px; } 17 | } 18 | @-moz-keyframes off-canvas { 19 | 0% { opacity: 0; } 20 | 100% { opacity: 1; } 21 | } 22 | @-o-keyframes off-canvas { 23 | 0% { opacity: 0; } 24 | 100% { opacity: 1; } 25 | } 26 | @keyframes off-canvas { 27 | 0% { opacity: 0; } 28 | 100% { opacity: 1; } 29 | } 30 | 31 | @include keyframes(fadeIn) { 32 | from { 33 | opacity: 0; 34 | } 35 | to { 36 | opacity: 1; 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /core/server/middleware.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('underscore'), 3 | express = require('express'), 4 | path = require('path'); 5 | 6 | function isBlackListedFileType(file) { 7 | var blackListedFileTypes = ['.hbs', '.md', '.txt', '.json'], 8 | ext = path.extname(file); 9 | return _.contains(blackListedFileTypes, ext); 10 | } 11 | 12 | var middleware = { 13 | 14 | staticTheme: function (g) { 15 | var ghost = g; 16 | return function blackListStatic(req, res, next) { 17 | if (isBlackListedFileType(req.url)) { 18 | return next(); 19 | } 20 | 21 | return middleware.forwardToExpressStatic(ghost, req, res, next); 22 | }; 23 | }, 24 | 25 | // to allow unit testing 26 | forwardToExpressStatic: function (ghost, req, res, next) { 27 | return express['static'](ghost.paths().activeTheme)(req, res, next); 28 | } 29 | }; 30 | 31 | module.exports = middleware; 32 | -------------------------------------------------------------------------------- /core/shared/lang/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "__SECTION__": "admin core", 3 | "admin.navbar.blog": "Blog", 4 | "admin.navbar.settings": "Settings", 5 | 6 | "__SECTION__": "icons", 7 | "icon.tag.label": "Tag", 8 | "icon.faq.label": "?", 9 | "icon.faq.markdown.title": "What is Markdown?", 10 | "icon.full_screen.label": "Full Screen", 11 | "icon.full_screen.title": "Enter full screen mode", 12 | "icon.settings.label": "Settings", 13 | 14 | "__SECTION__": "editor", 15 | "editor.entry_title.placeholder": "Your Post Title", 16 | "editor.entry_permalink.label": "Permalink:", 17 | "editor.entry_permalink.example_url": "http://yoursite.com/", 18 | "editor.entry_permalink.example_slug": "the-post-title-goes-here", 19 | "editor.headers.markdown.label": "Markdown", 20 | "editor.headers.preview.label": "Preview", 21 | "editor.word_count": "%{count} words", 22 | "editor.actions.save_draft": "Save Draft", 23 | "editor.actions.publish": "Publish" 24 | 25 | } 26 | -------------------------------------------------------------------------------- /core/server/models/role.js: -------------------------------------------------------------------------------- 1 | var User = require('./user').User, 2 | Permission = require('./permission').Permission, 3 | GhostBookshelf = require('./base'), 4 | Role, 5 | Roles; 6 | 7 | Role = GhostBookshelf.Model.extend({ 8 | 9 | tableName: 'roles', 10 | 11 | permittedAttributes: ['id', 'uuid', 'name', 'description', 'created_at', 'created_by', 'updated_at', 'updated_by'], 12 | 13 | validate: function () { 14 | GhostBookshelf.validator.check(this.get('name'), "Role name cannot be blank").notEmpty(); 15 | GhostBookshelf.validator.check(this.get('description'), "Role description cannot be blank").notEmpty(); 16 | }, 17 | 18 | users: function () { 19 | return this.belongsToMany(User); 20 | }, 21 | 22 | permissions: function () { 23 | return this.belongsToMany(Permission); 24 | } 25 | }); 26 | 27 | Roles = GhostBookshelf.Collection.extend({ 28 | model: Role 29 | }); 30 | 31 | module.exports = { 32 | Role: Role, 33 | Roles: Roles 34 | }; 35 | -------------------------------------------------------------------------------- /core/client/tpl/modal.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 14 |
    -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | b-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | .idea/* 18 | *.iml 19 | projectFilesBackup 20 | 21 | .DS_Store 22 | *~ 23 | *.swp 24 | *.swo 25 | 26 | # Ghost DB file 27 | *.db 28 | *.db-journal 29 | 30 | .build 31 | .dist 32 | 33 | /core/client/tpl/hbs-tpl.js 34 | /core/client/assets/css 35 | .sass-cache/ 36 | /core/client/assets/sass/config.rb 37 | /core/client/assets/sass/layouts/config.rb 38 | /core/client/assets/sass/modules/config.rb 39 | /core/client/assets/sass/modules/bourbon 40 | /core/client/assets/sass/modules/bourbon/* 41 | /core/server/data/export/exported* 42 | /docs 43 | /_site 44 | /content/data/* 45 | /content/plugins/**/* 46 | /content/themes/**/* 47 | /content/images/**/* 48 | !/content/themes/casper/** 49 | !README.md 50 | 51 | # Changelog, which is autogenerated, not committed 52 | CHANGELOG.md 53 | 54 | # Casper generated files 55 | /core/test/functional/*.png 56 | 57 | config.js 58 | 59 | # Built asset files 60 | /core/built -------------------------------------------------------------------------------- /core/shared/favicon.ico: -------------------------------------------------------------------------------- 1 |  ((  0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @ -------------------------------------------------------------------------------- /core/server/models/permission.js: -------------------------------------------------------------------------------- 1 | var GhostBookshelf = require('./base'), 2 | User = require('./user').User, 3 | Role = require('./role').Role, 4 | Permission, 5 | Permissions; 6 | 7 | Permission = GhostBookshelf.Model.extend({ 8 | 9 | tableName: 'permissions', 10 | 11 | permittedAttributes: ['id', 'uuid', 'name', 'object_type', 'action_type', 'object_id', 'created_at', 'created_by', 12 | 'updated_at', 'updated_by'], 13 | 14 | 15 | validate: function () { 16 | // TODO: validate object_type, action_type and object_id 17 | GhostBookshelf.validator.check(this.get('name'), "Permission name cannot be blank").notEmpty(); 18 | }, 19 | 20 | roles: function () { 21 | return this.belongsToMany(Role); 22 | }, 23 | 24 | users: function () { 25 | return this.belongsToMany(User); 26 | } 27 | }); 28 | 29 | Permissions = GhostBookshelf.Collection.extend({ 30 | model: Permission 31 | }); 32 | 33 | module.exports = { 34 | Permission: Permission, 35 | Permissions: Permissions 36 | }; -------------------------------------------------------------------------------- /core/server/views/error.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
    3 |
    4 |
    5 | 6 |
    7 |
    8 |

    {{code}}

    9 |

    {{message}}

    10 |
    11 |
    12 |
    13 | {{#if stack}} 14 |
    15 |

    Stack Trace

    16 |

    {{message}}

    17 |
      18 | {{#foreach stack}} 19 |
    • 20 | at 21 | {{#if function}}{{function}}{{/if}} 22 | ({{at}}) 23 |
    • 24 | {{/foreach}} 25 |
    26 |
    27 | {{/if}} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Ghost Foundation - Released under The MIT License. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /core/server/models/tag.js: -------------------------------------------------------------------------------- 1 | var Tag, 2 | Tags, 3 | Posts = require('./post').Posts, 4 | GhostBookshelf = require('./base'); 5 | 6 | Tag = GhostBookshelf.Model.extend({ 7 | 8 | tableName: 'tags', 9 | 10 | permittedAttributes: [ 11 | 'id', 'uuid', 'name', 'slug', 'description', 'parent_id', 'meta_title', 'meta_description', 'created_at', 12 | 'created_by', 'updated_at', 'updated_by' 13 | ], 14 | 15 | validate: function () { 16 | 17 | return true; 18 | }, 19 | 20 | creating: function () { 21 | var self = this; 22 | 23 | GhostBookshelf.Model.prototype.creating.call(this); 24 | 25 | if (!this.get('slug')) { 26 | // Generating a slug requires a db call to look for conflicting slugs 27 | return this.generateSlug(Tag, this.get('name')) 28 | .then(function (slug) { 29 | self.set({slug: slug}); 30 | }); 31 | } 32 | }, 33 | 34 | posts: function () { 35 | return this.belongsToMany(Posts); 36 | } 37 | }); 38 | 39 | Tags = GhostBookshelf.Collection.extend({ 40 | 41 | model: Tag 42 | 43 | }); 44 | 45 | module.exports = { 46 | Tag: Tag, 47 | Tags: Tags 48 | }; 49 | -------------------------------------------------------------------------------- /core/client/models/uploadModal.js: -------------------------------------------------------------------------------- 1 | /*global Ghost, Backbone, $ */ 2 | (function () { 3 | 'use strict'; 4 | Ghost.Models.uploadModal = Ghost.TemplateModel.extend({ 5 | 6 | options: { 7 | close: true, 8 | type: 'action', 9 | style: ["wide"], 10 | animation: 'fade', 11 | afterRender: function (id) { 12 | var filestorage = $('#' + this.options.model.id).data('filestorage'); 13 | this.$('.js-drop-zone').upload({fileStorage: filestorage}); 14 | }, 15 | confirm: { 16 | reject: { 17 | func: function () { // The function called on rejection 18 | return true; 19 | }, 20 | buttonClass: true, 21 | text: "Cancel" // The reject button text 22 | } 23 | } 24 | }, 25 | content: { 26 | template: 'uploadImage' 27 | }, 28 | 29 | initialize: function (options) { 30 | this.options.id = options.id; 31 | this.options.key = options.key; 32 | this.options.src = options.src; 33 | this.options.confirm.accept = options.accept; 34 | } 35 | }); 36 | 37 | }()); -------------------------------------------------------------------------------- /core/client/models/widget.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, $, _, Backbone */ 2 | (function () { 3 | 'use strict'; 4 | 5 | Ghost.Models.Widget = Ghost.TemplateModel.extend({ 6 | 7 | defaults: { 8 | title: '', 9 | name: '', 10 | author: '', 11 | applicationID: '', 12 | size: '', 13 | content: { 14 | template: '', 15 | data: { 16 | number: { 17 | count: 0, 18 | sub: { 19 | value: 0, 20 | dir: '', // "up" or "down" 21 | item: '', 22 | period: '' 23 | } 24 | } 25 | } 26 | }, 27 | settings: { 28 | settingsPane: false, 29 | enabled: false, 30 | options: [{ 31 | title: 'ERROR', 32 | value: 'Widget options not set' 33 | }] 34 | } 35 | } 36 | }); 37 | 38 | Ghost.Collections.Widgets = Backbone.Collection.extend({ 39 | // url: Ghost.settings.apiRoot + '/widgets/', // What will this be? 40 | model: Ghost.Models.Widget 41 | }); 42 | 43 | }()); -------------------------------------------------------------------------------- /core/server/plugins/GhostPlugin.js: -------------------------------------------------------------------------------- 1 | 2 | var GhostPlugin; 3 | 4 | /** 5 | * GhostPlugin is the base class for a standard plugin. 6 | * @class 7 | * @parameter {Ghost} The current Ghost app instance 8 | */ 9 | GhostPlugin = function (ghost) { 10 | this.app = ghost; 11 | }; 12 | 13 | /** 14 | * A method that will be called on installation. 15 | * Can optionally return a promise if async. 16 | * @parameter {Ghost} The current Ghost app instance 17 | */ 18 | GhostPlugin.prototype.install = function (ghost) { 19 | return; 20 | }; 21 | 22 | /** 23 | * A method that will be called on uninstallation. 24 | * Can optionally return a promise if async. 25 | * @parameter {Ghost} The current Ghost app instance 26 | */ 27 | GhostPlugin.prototype.uninstall = function (ghost) { 28 | return; 29 | }; 30 | 31 | /** 32 | * A method that will be called when the plugin is enabled. 33 | * Can optionally return a promise if async. 34 | * @parameter {Ghost} The current Ghost app instance 35 | */ 36 | GhostPlugin.prototype.activate = function (ghost) { 37 | return; 38 | }; 39 | 40 | /** 41 | * A method that will be called when the plugin is disabled. 42 | * Can optionally return a promise if async. 43 | * @parameter {Ghost} The current Ghost app instance 44 | */ 45 | GhostPlugin.prototype.deactivate = function (ghost) { 46 | return; 47 | }; 48 | 49 | module.exports = GhostPlugin; 50 | 51 | -------------------------------------------------------------------------------- /core/server/views/partials/navbar.hbs: -------------------------------------------------------------------------------- 1 | 2 | 26 | -------------------------------------------------------------------------------- /core/test/unit/api_posts_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, before, beforeEach, afterEach, it */ 2 | var testUtils = require('./testUtils'), 3 | should = require('should'), 4 | _ = require('underscore'); 5 | 6 | describe('Post API', function () { 7 | 8 | var user = testUtils.DataGenerator.forModel.users[0], 9 | authCookie; 10 | 11 | before(function (done) { 12 | testUtils.clearData() 13 | .then(function () { 14 | done(); 15 | }, done); 16 | }); 17 | 18 | beforeEach(function (done) { 19 | this.timeout(5000); 20 | testUtils.initData() 21 | .then(function () { 22 | return testUtils.insertDefaultFixtures(); 23 | }) 24 | .then(function () { 25 | return testUtils.API.login(user.email, user.password); 26 | }) 27 | .then(function (authResponse) { 28 | authCookie = authResponse; 29 | 30 | done(); 31 | }, done); 32 | }); 33 | 34 | afterEach(function (done) { 35 | testUtils.clearData().then(function () { 36 | done(); 37 | }, done); 38 | }); 39 | 40 | it('can retrieve a post', function (done) { 41 | testUtils.API.get(testUtils.API.ApiRouteBase + 'posts/?status=all', authCookie).then(function (result) { 42 | should.exist(result); 43 | should.exist(result.response); 44 | result.response.posts.length.should.be.above(1); 45 | done(); 46 | }).otherwise(done); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /core/shared/lang/i18n.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | /** 3 | * Create new Polyglot object 4 | * @type {Polyglot} 5 | */ 6 | I18n; 7 | 8 | I18n = function (ghost) { 9 | 10 | // TODO: validate 11 | var lang = ghost.settings('defaultLang'), 12 | path = ghost.paths().lang, 13 | langFilePath = path + lang + '.json'; 14 | 15 | return function (req, res, next) { 16 | 17 | if (lang === 'en_US') { 18 | // TODO: do stuff here to optimise for en 19 | 20 | // Make jslint empty block error go away 21 | lang = 'en_US'; 22 | } 23 | 24 | /** TODO: potentially use req.acceptedLanguages rather than the default 25 | * TODO: handle loading language file for frontend on frontend request etc 26 | * TODO: switch this mess to be promise driven */ 27 | fs.stat(langFilePath, function (error) { 28 | if (error) { 29 | console.log('No language file found for language ' + lang + '. Defaulting to en_US'); 30 | lang = 'en_US'; 31 | } 32 | 33 | fs.readFile(langFilePath, function (error, data) { 34 | if (error) { 35 | throw error; 36 | } 37 | 38 | try { 39 | data = JSON.parse(data); 40 | } catch (e) { 41 | throw e; // TODO: do something better with the error here 42 | } 43 | 44 | ghost.polyglot().extend(data); 45 | 46 | next(); 47 | }); 48 | }); 49 | }; 50 | }; 51 | 52 | 53 | module.exports.load = I18n; -------------------------------------------------------------------------------- /core/server/data/default-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": { 3 | "databaseVersion": { 4 | "defaultValue": "000" 5 | } 6 | }, 7 | "blog": { 8 | "title": { 9 | "defaultValue": "Ghost" 10 | }, 11 | "description": { 12 | "defaultValue": "Just a blogging platform." 13 | }, 14 | "email": { 15 | "defaultValue": "ghost@example.com", 16 | "validations": { 17 | "notNull": true, 18 | "isEmail": true 19 | } 20 | }, 21 | "logo": { 22 | "defaultValue": "" 23 | }, 24 | "cover": { 25 | "defaultValue": "" 26 | }, 27 | "defaultLang": { 28 | "defaultValue": "en_US", 29 | "validations": { 30 | "notNull": true 31 | } 32 | }, 33 | "postsPerPage": { 34 | "defaultValue": "6", 35 | "validations": { 36 | "notNull": true, 37 | "isInt": true, 38 | "max": 1000 39 | } 40 | }, 41 | "forceI18n": { 42 | "defaultValue": "true", 43 | "validations": { 44 | "notNull": true, 45 | "isIn": ["true", "false"] 46 | } 47 | } 48 | }, 49 | "theme": { 50 | "activeTheme": { 51 | "defaultValue": "casper" 52 | } 53 | }, 54 | "plugin": { 55 | "activePlugins": { 56 | "defaultValue": "[]" 57 | }, 58 | "installedPlugins": { 59 | "defaultValue": "[]" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/server/views/debug.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
    3 | 13 |
    14 |
    15 |

    General

    16 |
    17 |
    18 |
    19 |
    20 |
    21 | 22 | Export 23 |

    Export the blog settings and data.

    24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 | 31 | 32 | 33 |

    Import from another Ghost installation. If you import a user, this will replace the current user & log you out.

    34 |
    35 |
    36 |
    37 |
    38 |
    39 |
    -------------------------------------------------------------------------------- /core/client/tpl/preview.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | {{! TODO: JavaScript toggle featured/unfeatured}} 7 | {{#if published}}Published{{else}}Written{{/if}} 8 | by 9 | {{#if author.name}}{{author.name}}{{else}}{{author.email}}{{/if}} 10 |
    11 | 12 | 13 |
      14 |
    • 15 |
      16 | 17 |
      18 |
      19 | 20 |
      21 |
    • 22 |
    • 23 |
      24 | 25 |
      26 |
      27 | 28 |
      29 |
    • 30 |
    • 31 |
    32 |
    33 |
    34 |
    35 |

    {{{title}}}

    {{{html}}}
    36 |
    -------------------------------------------------------------------------------- /core/client/init.js: -------------------------------------------------------------------------------- 1 | /*globals window, $, _, Backbone, Validator */ 2 | (function () { 3 | 'use strict'; 4 | 5 | var Ghost = { 6 | Layout : {}, 7 | Views : {}, 8 | Collections : {}, 9 | Models : {}, 10 | Validate : new Validator(), 11 | 12 | settings: { 13 | apiRoot: '/api/v0.1' 14 | }, 15 | 16 | // This is a helper object to denote legacy things in the 17 | // middle of being transitioned. 18 | temporary: {}, 19 | 20 | currentView: null, 21 | router: null 22 | }; 23 | 24 | _.extend(Ghost, Backbone.Events); 25 | 26 | Ghost.init = function () { 27 | Ghost.router = new Ghost.Router(); 28 | 29 | // This is needed so Backbone recognizes elements already rendered server side 30 | // as valid views, and events are bound 31 | Ghost.notifications = new Ghost.Views.NotificationCollection({model: []}); 32 | 33 | Backbone.history.start({ 34 | pushState: true, 35 | hashChange: false, 36 | root: '/ghost' 37 | }); 38 | }; 39 | 40 | Ghost.Validate.error = function (object) { 41 | this._errors.push(object); 42 | 43 | return this; 44 | }; 45 | 46 | Ghost.Validate.handleErrors = function () { 47 | Ghost.notifications.clearEverything(); 48 | _.each(Ghost.Validate._errors, function (errorObj) { 49 | 50 | Ghost.notifications.addItem({ 51 | type: 'error', 52 | message: errorObj.message || errorObj, 53 | status: 'passive' 54 | }); 55 | if (errorObj.hasOwnProperty('el')) { 56 | errorObj.el.addClass('input-error'); 57 | } 58 | }); 59 | }; 60 | 61 | window.Ghost = Ghost; 62 | 63 | }()); 64 | -------------------------------------------------------------------------------- /core/client/toggle.js: -------------------------------------------------------------------------------- 1 | // # Toggle Support 2 | 3 | /*global document, $, Ghost */ 4 | (function () { 5 | 'use strict'; 6 | 7 | Ghost.temporary.hideToggles = function () { 8 | $('[data-toggle]').each(function () { 9 | var toggle = $(this).data('toggle'); 10 | $(this).parent().children(toggle + ':visible').fadeOut(); 11 | }); 12 | 13 | // Toggle active classes on menu headers 14 | $('[data-toggle].active').removeClass('active'); 15 | }; 16 | 17 | Ghost.temporary.initToggles = function ($el) { 18 | 19 | $el.find('[data-toggle]').each(function () { 20 | var toggle = $(this).data('toggle'); 21 | $(this).parent().children(toggle).hide(); 22 | }); 23 | 24 | $el.find('[data-toggle]').on('click', function (e) { 25 | e.preventDefault(); 26 | e.stopPropagation(); 27 | var $this = $(this), 28 | toggle = $this.data('toggle'), 29 | isAlreadyActive = $this.is('.active'); 30 | 31 | // Close all the other open toggle menus 32 | Ghost.temporary.hideToggles(); 33 | 34 | if (!isAlreadyActive) { 35 | $this.toggleClass('active'); 36 | $(this).parent().children(toggle).toggleClass('open').fadeToggle(200); 37 | } 38 | }); 39 | 40 | }; 41 | 42 | 43 | $(document).ready(function () { 44 | 45 | // ## Toggle Up In Your Grill 46 | // Allows for toggling via data-attributes. 47 | // ### Usage 48 | // 54 | Ghost.temporary.initToggles($(document)); 55 | }); 56 | 57 | }()); 58 | -------------------------------------------------------------------------------- /core/test/unit/plugins_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, beforeEach, afterEach, before, it*/ 2 | var testUtils = require('./testUtils'), 3 | should = require('should'), 4 | sinon = require('sinon'), 5 | _ = require("underscore"), 6 | when = require('when'), 7 | errors = require('../../server/errorHandling'), 8 | 9 | // Stuff we are testing 10 | plugins = require('../../server/plugins'), 11 | GhostPlugin = plugins.GhostPlugin, 12 | loader = require('../../server/plugins/loader'); 13 | 14 | describe('Plugins', function () { 15 | 16 | var sandbox; 17 | 18 | before(function (done) { 19 | testUtils.clearData().then(function () { 20 | done(); 21 | }, done); 22 | }); 23 | 24 | beforeEach(function (done) { 25 | this.timeout(5000); 26 | sandbox = sinon.sandbox.create(); 27 | 28 | testUtils.initData().then(function () { 29 | done(); 30 | }, done); 31 | }); 32 | 33 | afterEach(function (done) { 34 | sandbox.restore(); 35 | 36 | testUtils.clearData().then(function () { 37 | done(); 38 | }, done); 39 | }); 40 | 41 | describe('GhostPlugin Class', function () { 42 | 43 | should.exist(GhostPlugin); 44 | 45 | it('sets app instance', function () { 46 | var fakeGhost = {fake: true}, 47 | plugin = new GhostPlugin(fakeGhost); 48 | 49 | plugin.app.should.equal(fakeGhost); 50 | }); 51 | 52 | it('has default install, uninstall, activate and deactivate methods', function () { 53 | var fakeGhost = {fake: true}, 54 | plugin = new GhostPlugin(fakeGhost); 55 | 56 | _.isFunction(plugin.install).should.equal(true); 57 | _.isFunction(plugin.uninstall).should.equal(true); 58 | _.isFunction(plugin.activate).should.equal(true); 59 | _.isFunction(plugin.deactivate).should.equal(true); 60 | }); 61 | }); 62 | }); -------------------------------------------------------------------------------- /core/client/assets/sass/screen.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Welcome to Ghost - all styles for the Ghost platform are located within 3 | * this set of Sass files. Use this file like a table of contents. 4 | */ 5 | 6 | /* ========================================================================== 7 | Modules - These styles are re-used in many areas, and are grouped by type. 8 | ========================================================================== */ 9 | 10 | @import "modules/mixins"; 11 | /* Sass variables like colours, font sizes, basic styles. */ 12 | 13 | @import "modules/normalize"; 14 | /* Browser cross compatibility normalisation*/ 15 | 16 | @import "modules/icons"; 17 | /* All the styles controlling icons. */ 18 | 19 | @import "modules/animations"; 20 | /* Keyframe animations. */ 21 | 22 | @import "modules/global"; 23 | /* Global elements for the UI, like the header and footer. */ 24 | 25 | @import "modules/forms"; 26 | /* All the styles controlling forms and form fields. */ 27 | 28 | 29 | 30 | /* ========================================================================== 31 | Layouts - Styles for specific admin screen layouts, grouped by screen. 32 | ========================================================================== */ 33 | 34 | @import "layouts/manage"; 35 | /* The manage posts screen. */ 36 | 37 | @import "layouts/editor"; 38 | /* The write/edit post screen. */ 39 | 40 | @import "layouts/auth"; 41 | /* The login screen. */ 42 | 43 | @import "layouts/errors"; 44 | /* The error screens. */ 45 | 46 | /* ========================================================================== 47 | Settings Layouts - Styles for the individual settings panes, grouped by pane. 48 | ========================================================================== */ 49 | @import "layouts/settings"; 50 | /* The settings screen. */ 51 | 52 | @import "layouts/users"; 53 | /* The users pane. */ 54 | 55 | @import "layouts/plugins"; 56 | /* The plugins pane. */ -------------------------------------------------------------------------------- /core/client/assets/vendor/showdown/extensions/ghostdown.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var ghostdown = function () { 3 | return [ 4 | // ![] image syntax 5 | { 6 | type: 'lang', 7 | filter: function (text) { 8 | var imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim, 9 | /* regex from isURL in node-validator. Yum! */ 10 | uriRegex = /^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i, 11 | pathRegex = /^(\/)?([^\/\0]+(\/)?)+$/i; 12 | 13 | return text.replace(imageMarkdownRegex, function (match, key, alt, src) { 14 | var result = ""; 15 | 16 | if (src && (src.match(uriRegex) || src.match(pathRegex))) { 17 | result = ''; 18 | } 19 | return '
    ' + result + 20 | '
    Add image of ' + alt + '
    ' + 21 | '' + 22 | '
    '; 23 | }); 24 | } 25 | } 26 | ]; 27 | }; 28 | 29 | // Client-side export 30 | if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) { 31 | window.Showdown.extensions.ghostdown = ghostdown; 32 | } 33 | // Server-side export 34 | if (typeof module !== 'undefined') module.exports = ghostdown; 35 | }()); -------------------------------------------------------------------------------- /core/server/data/export/index.js: -------------------------------------------------------------------------------- 1 | var when = require('when'), 2 | _ = require('underscore'), 3 | migration = require('../migration'), 4 | client = require('../../models/base').client, 5 | knex = require('../../models/base').Knex, 6 | 7 | exporter; 8 | 9 | function getTablesFromSqlite3() { 10 | return knex.Raw("select * from sqlite_master where type = 'table'").then(function (response) { 11 | return _.reject(_.pluck(response, 'tbl_name'), function (name) { 12 | return name === 'sqlite_sequence'; 13 | }); 14 | }); 15 | } 16 | 17 | function getTablesFromMySQL() { 18 | return knex.Raw('show tables').then(function (response) { 19 | return _.flatten(_.map(response, function (entry) { 20 | return _.values(entry); 21 | })); 22 | }); 23 | } 24 | 25 | exporter = function () { 26 | var tablesToExport; 27 | 28 | if (client === 'sqlite3') { 29 | tablesToExport = getTablesFromSqlite3(); 30 | } else if (client === 'mysql') { 31 | tablesToExport = getTablesFromMySQL(); 32 | } else { 33 | return when.reject("No exporter for database client " + client); 34 | } 35 | 36 | return when.join(migration.getDatabaseVersion(), tablesToExport).then(function (results) { 37 | var version = results[0], 38 | tables = results[1], 39 | selectOps = _.map(tables, function (name) { 40 | return knex(name).select(); 41 | }); 42 | 43 | return when.all(selectOps).then(function (tableData) { 44 | var exportData = { 45 | meta: { 46 | exported_on: new Date().getTime(), 47 | version: version 48 | }, 49 | data: { 50 | // Filled below 51 | } 52 | }; 53 | 54 | _.each(tables, function (name, i) { 55 | exportData.data[name] = tableData[i]; 56 | }); 57 | 58 | return when.resolve(exportData); 59 | }, function (err) { 60 | console.log("Error exporting data: " + err); 61 | }); 62 | }); 63 | }; 64 | 65 | module.exports = exporter; -------------------------------------------------------------------------------- /core/server/views/user-error.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{code}} — {{message}} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {{{block "pageStyles"}}} 22 | 23 | 24 |
    25 |
    26 |
    27 |
    28 | 32 |
    33 |
    34 |

    {{code}}

    35 |

    {{message}}

    36 |
    37 |
    38 |
    39 | {{#if stack}} 40 |
    41 |

    Stack Trace

    42 |

    {{message}}

    43 |
      44 | {{#foreach stack}} 45 |
    • 46 | at 47 | {{#if function}}{{function}}{{/if}} 48 | ({{at}}) 49 |
    • 50 | {{/foreach}} 51 |
    52 |
    53 | {{/if}} 54 |
    55 | 56 | -------------------------------------------------------------------------------- /core/server/require-tree.js: -------------------------------------------------------------------------------- 1 | var when = require('when'), 2 | keys = require('when/keys'), 3 | fs = require('fs'), 4 | path = require('path'), 5 | extend = function (obj, source) { 6 | var key; 7 | for (key in source) { 8 | if (source.hasOwnProperty(key)) { 9 | obj[key] = source[key]; 10 | } 11 | } 12 | return obj; 13 | }, 14 | readDir = function (dir, options, depth) { 15 | depth = depth || 0; 16 | 17 | options = extend({ 18 | index: true 19 | }, options); 20 | 21 | if (depth > 1) { 22 | return null; 23 | } 24 | 25 | var subtree = {}, 26 | treeDeferred = when.defer(), 27 | treePromise = treeDeferred.promise; 28 | 29 | fs.readdir(dir, function (error, files) { 30 | if (error) { 31 | return treeDeferred.reject(error); 32 | } 33 | 34 | files = files || []; 35 | 36 | files.forEach(function (file) { 37 | var fileDeferred = when.defer(), 38 | filePromise = fileDeferred.promise, 39 | ext = path.extname(file), 40 | name = path.basename(file, ext), 41 | fpath = path.join(dir, file); 42 | subtree[name] = filePromise; 43 | fs.lstat(fpath, function (error, result) { 44 | if (result.isDirectory()) { 45 | fileDeferred.resolve(readDir(fpath, options, depth + 1)); 46 | } else { 47 | fileDeferred.resolve(fpath); 48 | } 49 | }); 50 | }); 51 | 52 | return keys.all(subtree).then(function (theFiles) { 53 | return treeDeferred.resolve(theFiles); 54 | }); 55 | }); 56 | 57 | return when(treePromise).then(function (prom) { 58 | return prom; 59 | }); 60 | }, 61 | readAll = function (dir, options, depth) { 62 | return when(readDir(dir, options, depth)).then(function (paths) { 63 | return paths; 64 | }); 65 | }; 66 | 67 | module.exports = readAll; -------------------------------------------------------------------------------- /core/client/models/post.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, $, _, Backbone */ 2 | (function () { 3 | 'use strict'; 4 | 5 | Ghost.Models.Post = Ghost.TemplateModel.extend({ 6 | 7 | defaults: { 8 | status: 'draft' 9 | }, 10 | 11 | blacklist: ['published', 'draft'], 12 | 13 | parse: function (resp) { 14 | if (resp.status) { 15 | resp.published = !!(resp.status === 'published'); 16 | resp.draft = !!(resp.status === 'draft'); 17 | } 18 | if (resp.tags) { 19 | // TODO: parse tags into it's own collection on the model (this.tags) 20 | return resp; 21 | } 22 | return resp; 23 | }, 24 | 25 | validate: function (attrs) { 26 | if (_.isEmpty(attrs.title)) { 27 | return 'You must specify a title for the post.'; 28 | } 29 | }, 30 | 31 | addTag: function (tagToAdd) { 32 | var tags = this.get('tags') || []; 33 | tags.push(tagToAdd); 34 | this.set('tags', tags); 35 | }, 36 | 37 | removeTag: function (tagToRemove) { 38 | var tags = this.get('tags') || []; 39 | tags = _.reject(tags, function (tag) { 40 | return tag.id === tagToRemove.id || tag.name === tagToRemove.name; 41 | }); 42 | this.set('tags', tags); 43 | } 44 | }); 45 | 46 | Ghost.Collections.Posts = Backbone.Collection.extend({ 47 | currentPage: 1, 48 | totalPages: 0, 49 | totalPosts: 0, 50 | nextPage: 0, 51 | prevPage: 0, 52 | 53 | url: Ghost.settings.apiRoot + '/posts/', 54 | model: Ghost.Models.Post, 55 | 56 | parse: function (resp) { 57 | if (_.isArray(resp.posts)) { 58 | this.limit = resp.limit; 59 | this.currentPage = resp.page; 60 | this.totalPages = resp.pages; 61 | this.totalPosts = resp.total; 62 | this.nextPage = resp.next; 63 | this.prevPage = resp.prev; 64 | return resp.posts; 65 | } 66 | return resp; 67 | } 68 | }); 69 | 70 | }()); 71 | -------------------------------------------------------------------------------- /core/test/unit/export_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, before, beforeEach, afterEach, it*/ 2 | var testUtils = require('./testUtils'), 3 | should = require('should'), 4 | sinon = require('sinon'), 5 | when = require('when'), 6 | _ = require("underscore"), 7 | errors = require('../../server/errorHandling'), 8 | 9 | // Stuff we are testing 10 | migration = require('../../server/data/migration'), 11 | exporter = require('../../server/data/export'), 12 | Settings = require('../../server/models/settings').Settings; 13 | 14 | describe("Exporter", function () { 15 | 16 | should.exist(exporter); 17 | 18 | before(function (done) { 19 | testUtils.clearData().then(function () { 20 | done(); 21 | }, done); 22 | }); 23 | 24 | beforeEach(function (done) { 25 | this.timeout(5000); 26 | testUtils.initData().then(function () { 27 | done(); 28 | }, done); 29 | }); 30 | 31 | afterEach(function (done) { 32 | testUtils.clearData().then(function () { 33 | done(); 34 | }, done); 35 | }); 36 | 37 | it("exports data", function (done) { 38 | // Stub migrations to return 000 as the current database version 39 | var migrationStub = sinon.stub(migration, "getDatabaseVersion", function () { 40 | return when.resolve("000"); 41 | }); 42 | 43 | exporter().then(function (exportData) { 44 | var tables = ['posts', 'users', 'roles', 'roles_users', 'permissions', 'permissions_roles', 'permissions_users', 45 | 'settings', 'tags', 'posts_tags']; 46 | 47 | should.exist(exportData); 48 | 49 | should.exist(exportData.meta); 50 | should.exist(exportData.data); 51 | 52 | exportData.meta.version.should.equal("000"); 53 | _.findWhere(exportData.data.settings, {key: "databaseVersion"}).value.should.equal("000"); 54 | 55 | _.each(tables, function (name) { 56 | should.exist(exportData.data[name]); 57 | }); 58 | // should not export sqlite data 59 | should.not.exist(exportData.data.sqlite_sequence); 60 | 61 | migrationStub.restore(); 62 | done(); 63 | }).then(null, done); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /core/server/views/default.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Ghost Admin 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{{block "pageStyles"}}} 32 | 33 | 34 | {{#unless hideNavbar}} 35 | {{> navbar}} 36 | {{/unless}} 37 | 38 |
    39 | 42 | 43 | {{{body}}} 44 | 45 |
    46 | 47 | 49 | 50 | 51 | {{{ghostScriptTags}}} 52 | 53 | {{{block "bodyScripts"}}} 54 | 55 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /core/client/assets/sass/layouts/errors.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * These styles control elements specific to the error screens 3 | * 4 | * Table of Contents: 5 | * 6 | * General 7 | * 404 8 | */ 9 | 10 | /* ============================================================================= 11 | General 12 | ============================================================================= */ 13 | .error-content { 14 | max-width: 530px; 15 | margin: 0 auto; 16 | padding: 0; 17 | display: table; 18 | height: 100%; 19 | 20 | @include breakpoint(630px) { 21 | max-width: 264px; 22 | text-align: center; 23 | } 24 | } 25 | 26 | .error-details { 27 | display: table-cell; 28 | vertical-align: middle; 29 | } 30 | 31 | .error-image { 32 | display: inline-block; 33 | vertical-align: middle; 34 | width: 96px; 35 | height: 150px; 36 | 37 | @include breakpoint(630px) { 38 | width: 72px; 39 | height: 112px; 40 | } 41 | 42 | img { 43 | width: 100%; 44 | height: 100%; 45 | } 46 | } 47 | 48 | .error-message { 49 | position: relative; 50 | top: -5px; 51 | display: inline-block; 52 | vertical-align: middle; 53 | margin-left: 10px; 54 | } 55 | 56 | .error-code { 57 | margin: 0; 58 | font-size: 7.8em; 59 | line-height: 0.9em; 60 | color: #979797; 61 | 62 | @include breakpoint(630px) { 63 | font-size: 5.8em; 64 | } 65 | } 66 | 67 | .error-description { 68 | margin: 0; 69 | padding: 0; 70 | font-weight: 300; 71 | font-size: 1.9em; 72 | color: #979797; 73 | border: none; 74 | 75 | @include breakpoint(630px) { 76 | font-size: 1.4em; 77 | } 78 | } 79 | 80 | .error-stack { 81 | margin: 1em auto; 82 | padding: 2em; 83 | max-width: 800px; 84 | background-color: rgba(255,255,255,0.3); 85 | } 86 | 87 | .error-stack-list { 88 | list-style-type: none; 89 | padding: 0; 90 | margin: 0; 91 | } 92 | 93 | .error-stack-list li { 94 | display: block; 95 | 96 | &::before { 97 | color: #BBB; 98 | content: "↪︎"; 99 | display: inline-block; 100 | font-size: 1.2em; 101 | margin-right: 0.5em; 102 | } 103 | } 104 | 105 | .error-stack-function { 106 | font-weight: bold; 107 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "ghost", 3 | "version" : "0.3.2", 4 | "description" : "Just a blogging platform.", 5 | "author" : "Ghost Foundation", 6 | "homepage" : "http://ghost.org", 7 | "keywords" : ["ghost", "blog", "cms"], 8 | "repository" : {"type": "git", "url": "git://github.com/TryGhost/Ghost.git"}, 9 | "private" : true, 10 | "licenses" : [ 11 | { 12 | "type": "MIT", 13 | "url": "https://raw.github.com/TryGhost/Ghost/master/LICENSE" 14 | } 15 | ], 16 | "scripts": { 17 | "start": "node index", 18 | "test": "grunt validate --verbose" 19 | }, 20 | "engines": { 21 | "node": ">=0.10.* <0.11.4" 22 | }, 23 | "engineStrict": true, 24 | "dependencies": { 25 | "express": "3.3.4", 26 | "express-hbs": "0.2.2", 27 | "connect-slashes": "0.0.9", 28 | "node-polyglot": "0.2.1", 29 | "moment": "2.1.0", 30 | "underscore": "1.5.1", 31 | "showdown": "0.3.1", 32 | "sqlite3": "2.1.16", 33 | "bookshelf": "0.3.1", 34 | "knex": "0.2.7-alpha", 35 | "when": "2.2.1", 36 | "bcrypt-nodejs": "0.0.3", 37 | "node-uuid": "1.4.0", 38 | "colors": "0.6.1", 39 | "semver": "2.1.0", 40 | "fs-extra": "0.6.3", 41 | "downsize": "0.0.2", 42 | "validator": "1.4.0", 43 | "rss": "0.2.0", 44 | "nodemailer": "0.5.2" 45 | }, 46 | "optionalDependencies": { 47 | "mysql": "2.0.0-alpha9" 48 | }, 49 | "devDependencies": { 50 | "grunt": "~0.4.1", 51 | "grunt-jslint": "~1.0.0", 52 | "grunt-shell": "~0.3.1", 53 | "grunt-contrib-sass": "~0.4.1", 54 | "grunt-contrib-handlebars": "~0.5.10", 55 | "grunt-contrib-watch": "~0.5.1", 56 | "grunt-bump": "~0.0.11", 57 | "grunt-contrib-clean": "~0.5.0", 58 | "grunt-contrib-copy": "~0.4.1", 59 | "grunt-contrib-compress": "~0.5.2", 60 | "grunt-contrib-concat": "~0.3.0", 61 | "grunt-contrib-uglify": "~0.2.4", 62 | "grunt-groc": "~0.3.0", 63 | "grunt-mocha-cli": "~1.0.6", 64 | "grunt-express-server": "~0.4.2", 65 | "grunt-open": "~0.2.2", 66 | "matchdep": "~0.1.2", 67 | "sinon": "~1.7.3", 68 | "should": "~1.2.2", 69 | "mocha": "~1.12.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /core/client/assets/vendor/codemirror/addon/mode/overlay.js: -------------------------------------------------------------------------------- 1 | // Utility function that allows modes to be combined. The mode given 2 | // as the base argument takes care of most of the normal mode 3 | // functionality, but a second (typically simple) mode is used, which 4 | // can override the style of text. Both modes get to parse all of the 5 | // text, but when both assign a non-null style to a piece of code, the 6 | // overlay wins, unless the combine argument was true, in which case 7 | // the styles are combined. 8 | 9 | // overlayParser is the old, deprecated name 10 | CodeMirror.overlayMode = CodeMirror.overlayParser = function(base, overlay, combine) { 11 | return { 12 | startState: function() { 13 | return { 14 | base: CodeMirror.startState(base), 15 | overlay: CodeMirror.startState(overlay), 16 | basePos: 0, baseCur: null, 17 | overlayPos: 0, overlayCur: null 18 | }; 19 | }, 20 | copyState: function(state) { 21 | return { 22 | base: CodeMirror.copyState(base, state.base), 23 | overlay: CodeMirror.copyState(overlay, state.overlay), 24 | basePos: state.basePos, baseCur: null, 25 | overlayPos: state.overlayPos, overlayCur: null 26 | }; 27 | }, 28 | 29 | token: function(stream, state) { 30 | if (stream.start == state.basePos) { 31 | state.baseCur = base.token(stream, state.base); 32 | state.basePos = stream.pos; 33 | } 34 | if (stream.start == state.overlayPos) { 35 | stream.pos = stream.start; 36 | state.overlayCur = overlay.token(stream, state.overlay); 37 | state.overlayPos = stream.pos; 38 | } 39 | stream.pos = Math.min(state.basePos, state.overlayPos); 40 | if (stream.eol()) state.basePos = state.overlayPos = 0; 41 | 42 | if (state.overlayCur == null) return state.baseCur; 43 | if (state.baseCur != null && combine) return state.baseCur + " " + state.overlayCur; 44 | else return state.overlayCur; 45 | }, 46 | 47 | indent: base.indent && function(state, textAfter) { 48 | return base.indent(state.base, textAfter); 49 | }, 50 | electricChars: base.electricChars, 51 | 52 | innerMode: function(state) { return {state: state.base, mode: base}; }, 53 | 54 | blankLine: function(state) { 55 | if (base.blankLine) base.blankLine(state.base); 56 | if (overlay.blankLine) overlay.blankLine(state.overlay); 57 | } 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /core/client/assets/vendor/codemirror/mode/gfm/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CodeMirror: GFM mode 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |

    CodeMirror: GFM mode

    24 | 25 |
    60 | 61 | 68 | 69 |

    Optionally depends on other modes for properly highlighted code blocks.

    70 | 71 |

    Parsing/Highlighting Tests: normal, verbose.

    72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /core/test/functional/admin/flow_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests the flow of creating, editing and publishing tests 3 | */ 4 | 5 | /*globals casper, __utils__, url, testPost */ 6 | CasperTest.begin("Ghost edit draft flow works correctly", 8, function suite(test) { 7 | casper.thenOpen(url + "ghost/editor/", function then() { 8 | test.assertUrlMatch(/ghost\/editor\/$/, "Ghost doesn't require login this time"); 9 | }); 10 | 11 | casper.then(function createTestPost() { 12 | casper.sendKeys('#entry-title', testPost.title); 13 | casper.writeContentToCodeMirror(testPost.html); 14 | }); 15 | 16 | casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() { 17 | test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct'); 18 | }); 19 | 20 | casper.thenClick('.js-publish-button'); 21 | casper.waitForResource(/posts/); 22 | 23 | casper.waitForSelector('.notification-success', function onSuccess() { 24 | test.assert(true, 'Got success notification'); 25 | }, function onTimeout() { 26 | test.assert(false, 'No success notification :('); 27 | }); 28 | 29 | casper.thenOpen(url + 'ghost/content/', function then() { 30 | test.assertUrlMatch(/ghost\/content\//, "Ghost successfully loaded the content page"); 31 | }); 32 | 33 | casper.then(function then() { 34 | test.assertEvalEquals(function () { 35 | return document.querySelector('.content-list-content li').className; 36 | }, "active", "first item is active"); 37 | 38 | test.assertSelectorHasText(".content-list-content li:first-child h3", testPost.title, "first item is the post we created"); 39 | }); 40 | 41 | casper.thenClick('.post-edit').waitForResource(/editor/, function then() { 42 | test.assertUrlMatch(/editor/, "Ghost sucessfully loaded the editor page again"); 43 | }); 44 | 45 | casper.thenClick('.js-publish-button'); 46 | casper.waitForResource(/posts/); 47 | 48 | casper.waitForSelector('.notification-success', function onSuccess() { 49 | test.assert(true, 'Got success notification'); 50 | }, function onTimeout() { 51 | test.assert(false, 'No success notification :('); 52 | }); 53 | }); 54 | 55 | // TODO: test publishing, editing, republishing, unpublishing etc 56 | //CasperTest.begin("Ghost edit published flow works correctly", 6, function suite(test) { 57 | // 58 | // 59 | // 60 | //}); -------------------------------------------------------------------------------- /core/test/unit/testUtils.js: -------------------------------------------------------------------------------- 1 | var knex = require('../../server/models/base').Knex, 2 | when = require('when'), 3 | migration = require("../../server/data/migration/"), 4 | Settings = require('../../server/models/settings').Settings, 5 | DataGenerator = require('./fixtures/data-generator'), 6 | API = require('./utils/api'); 7 | 8 | function initData() { 9 | return migration.init(); 10 | } 11 | 12 | function clearData() { 13 | // we must always try to delete all tables 14 | return migration.reset(); 15 | } 16 | 17 | function insertDefaultFixtures() { 18 | var promises = []; 19 | 20 | promises.push(insertDefaultUser()); 21 | promises.push(insertPosts()); 22 | 23 | return when.all(promises); 24 | } 25 | 26 | function insertPosts() { 27 | var promises = []; 28 | 29 | promises.push(knex('posts').insert(DataGenerator.forKnex.posts)); 30 | promises.push(knex('tags').insert(DataGenerator.forKnex.tags)); 31 | promises.push(knex('posts_tags').insert(DataGenerator.forKnex.posts_tags)); 32 | 33 | return when.all(promises); 34 | } 35 | 36 | function insertMorePosts() { 37 | var lang, 38 | status, 39 | posts, 40 | promises = [], 41 | i, j, k = 0; 42 | 43 | for (i = 0; i < 2; i += 1) { 44 | posts = []; 45 | lang = i % 2 ? 'en' : 'fr'; 46 | posts.push(DataGenerator.forKnex.createGenericPost(k++, null, lang)); 47 | 48 | for (j = 0; j < 50; j += 1) { 49 | status = j % 2 ? 'published' : 'draft'; 50 | posts.push(DataGenerator.forKnex.createGenericPost(k++, status, lang)); 51 | } 52 | 53 | promises.push(knex('posts').insert(posts)); 54 | } 55 | 56 | return when.all(promises); 57 | } 58 | 59 | function insertDefaultUser() { 60 | var users = [], 61 | userRoles = [], 62 | u_promises = []; 63 | 64 | users.push(DataGenerator.forKnex.createUser(DataGenerator.Content.users[0])); 65 | u_promises.push(knex('users').insert(users)); 66 | userRoles.push(DataGenerator.forKnex.createUserRole(1, 1)); 67 | u_promises.push(knex('roles_users').insert(userRoles)); 68 | 69 | return when.all(u_promises); 70 | } 71 | 72 | module.exports = { 73 | initData: initData, 74 | clearData: clearData, 75 | insertDefaultFixtures: insertDefaultFixtures, 76 | insertPosts: insertPosts, 77 | insertMorePosts: insertMorePosts, 78 | insertDefaultUser: insertDefaultUser, 79 | 80 | DataGenerator: DataGenerator, 81 | API: API 82 | }; 83 | -------------------------------------------------------------------------------- /core/server/plugins/loader.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require('path'), 3 | _ = require('underscore'), 4 | when = require('when'), 5 | ghostInstance, 6 | loader; 7 | 8 | function getGhostInstance() { 9 | if (ghostInstance) { 10 | return ghostInstance; 11 | } 12 | 13 | var Ghost = require('../../ghost'); 14 | 15 | ghostInstance = new Ghost(); 16 | 17 | return ghostInstance; 18 | } 19 | 20 | // Get a relative path to the given plugins root, defaults 21 | // to be relative to __dirname 22 | function getPluginRelativePath(name, relativeTo, ghost) { 23 | ghost = ghost || getGhostInstance(); 24 | relativeTo = relativeTo || __dirname; 25 | 26 | return path.relative(relativeTo, path.join(ghost.paths().pluginPath, name)); 27 | } 28 | 29 | 30 | function getPluginByName(name, ghost) { 31 | ghost = ghost || getGhostInstance(); 32 | 33 | // Grab the plugin class to instantiate 34 | var PluginClass = require(getPluginRelativePath(name)), 35 | plugin; 36 | 37 | // Check for an actual class, otherwise just use whatever was returned 38 | if (_.isFunction(PluginClass)) { 39 | plugin = new PluginClass(ghost); 40 | } else { 41 | plugin = PluginClass; 42 | } 43 | 44 | return plugin; 45 | } 46 | 47 | // The loader is responsible for loading plugins 48 | loader = { 49 | // Load a plugin and return the instantiated plugin 50 | installPluginByName: function (name, ghost) { 51 | var plugin = getPluginByName(name, ghost); 52 | 53 | // Check for an install() method on the plugin. 54 | if (!_.isFunction(plugin.install)) { 55 | return when.reject(new Error("Error loading plugin named " + name + "; no install() method defined.")); 56 | } 57 | 58 | // Wrapping the install() with a when because it's possible 59 | // to not return a promise from it. 60 | return when(plugin.install(ghost)).then(function () { 61 | return when.resolve(plugin); 62 | }); 63 | }, 64 | 65 | // Activate a plugin and return it 66 | activatePluginByName: function (name, ghost) { 67 | var plugin = getPluginByName(name, ghost); 68 | 69 | // Check for an activate() method on the plugin. 70 | if (!_.isFunction(plugin.activate)) { 71 | return when.reject(new Error("Error loading plugin named " + name + "; no activate() method defined.")); 72 | } 73 | 74 | // Wrapping the activate() with a when because it's possible 75 | // to not return a promise from it. 76 | return when(plugin.activate(ghost)).then(function () { 77 | return when.resolve(plugin); 78 | }); 79 | } 80 | }; 81 | 82 | module.exports = loader; -------------------------------------------------------------------------------- /core/client/mobile-interactions.js: -------------------------------------------------------------------------------- 1 | // # Ghost Mobile Interactions 2 | 3 | /*global window, document, $, FastClick */ 4 | 5 | (function () { 6 | 'use strict'; 7 | 8 | FastClick.attach(document.body); 9 | 10 | // ### Show content preview when swiping left on content list 11 | $('.manage').on('click', '.content-list ol li', function (event) { 12 | if (window.matchMedia('(max-width: 800px)').matches) { 13 | event.preventDefault(); 14 | event.stopPropagation(); 15 | $('.content-list').animate({right: '100%', left: '-100%', 'margin-right': '15px'}, 300); 16 | $('.content-preview').animate({right: '0', left: '0', 'margin-left': '0'}, 300); 17 | } 18 | }); 19 | 20 | // ### Hide content preview 21 | $('.manage').on('click', '.content-preview .button-back', function (event) { 22 | if (window.matchMedia('(max-width: 800px)').matches) { 23 | event.preventDefault(); 24 | event.stopPropagation(); 25 | $('.content-list').animate({right: '0', left: '0', 'margin-right': '0'}, 300); 26 | $('.content-preview').animate({right: '-100%', left: '100%', 'margin-left': '15px'}, 300); 27 | } 28 | }); 29 | 30 | // ### Show settings options page when swiping left on settings menu link 31 | $('.settings').on('click', '.settings-menu li', function (event) { 32 | if (window.matchMedia('(max-width: 800px)').matches) { 33 | event.preventDefault(); 34 | event.stopPropagation(); 35 | $('.settings-sidebar').animate({right: '100%', left: '-102%', 'margin-right': '15px'}, 300); 36 | $('.settings-content').animate({right: '0', left: '0', 'margin-left': '0'}, 300); 37 | $('.settings-content .button-back, .settings-content .button-save').css('display', 'inline-block'); 38 | } 39 | }); 40 | 41 | // ### Hide settings options page 42 | $('.settings').on('click', '.settings-content .button-back', function (event) { 43 | if (window.matchMedia('(max-width: 800px)').matches) { 44 | event.preventDefault(); 45 | event.stopPropagation(); 46 | $('.settings-sidebar').animate({right: '0', left: '0', 'margin-right': '0'}, 300); 47 | $('.settings-content').animate({right: '-100%', left: '100%', 'margin-left': '15'}, 300); 48 | $('.settings-content .button-back, .settings-content .button-save').css('display', 'none'); 49 | } 50 | }); 51 | 52 | // ### Toggle the sidebar menu 53 | $('[data-off-canvas]').on('click', function (event) { 54 | if (window.matchMedia('(max-width: 650px)').matches) { 55 | event.preventDefault(); 56 | $('body').toggleClass('off-canvas'); 57 | } 58 | }); 59 | 60 | }()); -------------------------------------------------------------------------------- /core/client/router.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, Backbone, $, _, NProgress */ 2 | (function () { 3 | "use strict"; 4 | 5 | Ghost.Router = Backbone.Router.extend({ 6 | 7 | routes: { 8 | '' : 'blog', 9 | 'content/' : 'blog', 10 | 'settings(/:pane)/' : 'settings', 11 | 'editor(/:id)/' : 'editor', 12 | 'debug/' : 'debug', 13 | 'register/' : 'register', 14 | 'signup/' : 'signup', 15 | 'signin/' : 'login', 16 | 'forgotten/' : 'forgotten' 17 | }, 18 | 19 | signup: function () { 20 | Ghost.currentView = new Ghost.Views.Signup({ el: '.js-signup-box' }); 21 | }, 22 | 23 | login: function () { 24 | Ghost.currentView = new Ghost.Views.Login({ el: '.js-login-box' }); 25 | }, 26 | 27 | forgotten: function () { 28 | Ghost.currentView = new Ghost.Views.Forgotten({ el: '.js-forgotten-box' }); 29 | }, 30 | 31 | blog: function () { 32 | var posts = new Ghost.Collections.Posts(); 33 | NProgress.start(); 34 | posts.fetch({ data: { status: 'all', orderBy: ['updated_at', 'DESC'] } }).then(function () { 35 | Ghost.currentView = new Ghost.Views.Blog({ el: '#main', collection: posts }); 36 | NProgress.done(); 37 | }); 38 | }, 39 | 40 | settings: function (pane) { 41 | if (!pane) { 42 | // Redirect to settings/general if no pane supplied 43 | this.navigate('/settings/general/', { 44 | trigger: true, 45 | replace: true 46 | }); 47 | return; 48 | } 49 | 50 | // only update the currentView if we don't already have a Settings view 51 | if (!Ghost.currentView || !(Ghost.currentView instanceof Ghost.Views.Settings)) { 52 | Ghost.currentView = new Ghost.Views.Settings({ el: '#main', pane: pane }); 53 | } 54 | }, 55 | 56 | editor: function (id) { 57 | var post = new Ghost.Models.Post(); 58 | post.urlRoot = Ghost.settings.apiRoot + '/posts'; 59 | if (id) { 60 | post.id = id; 61 | post.fetch().then(function () { 62 | Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post }); 63 | }); 64 | } else { 65 | Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post }); 66 | } 67 | }, 68 | 69 | debug: function () { 70 | Ghost.currentView = new Ghost.Views.Debug({ el: "#main" }); 71 | } 72 | }); 73 | }()); 74 | -------------------------------------------------------------------------------- /core/test/unit/client_ghostdown_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test the ghostdown extension 3 | * 4 | * Only ever runs on the client (i.e in the editor) 5 | * Server processes showdown without it so there can never be an image upload form in a post. 6 | */ 7 | 8 | /*globals describe, it */ 9 | var testUtils = require('./testUtils'), 10 | should = require('should'), 11 | 12 | // Stuff we are testing 13 | gdPath = "../../client/assets/vendor/showdown/extensions/ghostdown.js", 14 | ghostdown = require(gdPath); 15 | 16 | describe("Ghostdown showdown extensions", function () { 17 | 18 | it("should export an array of methods for processing", function () { 19 | 20 | ghostdown.should.be.a("function"); 21 | ghostdown().should.be.an.instanceof(Array); 22 | 23 | ghostdown().forEach(function (processor) { 24 | processor.should.be.a("object"); 25 | processor.should.have.property("type"); 26 | processor.should.have.property("filter"); 27 | processor.type.should.be.a("string"); 28 | processor.filter.should.be.a("function"); 29 | }); 30 | }); 31 | 32 | it("should accurately detect images in markdown", function () { 33 | [ 34 | "![]", 35 | "![]()", 36 | "![image and another,/ image]", 37 | "![image and another,/ image]()", 38 | "![image and another,/ image](http://dsurl.stuff)", 39 | "![](http://dsurl.stuff)" 40 | /* No ref-style for now 41 | "![][]", 42 | "![image and another,/ image][stuff]", 43 | "![][stuff]", 44 | "![image and another,/ image][]" 45 | */ 46 | ] 47 | .forEach(function (imageMarkup) { 48 | var processedMarkup = 49 | ghostdown().reduce(function (prev, processor) { 50 | return processor.filter(prev); 51 | }, imageMarkup); 52 | 53 | // The image is the entire markup, so the image box should be too 54 | processedMarkup.should.match(/^\n*$/); 55 | }); 56 | }); 57 | 58 | it("should correctly include an image", function () { 59 | [ 60 | "![image and another,/ image](http://dsurl.stuff)", 61 | "![](http://dsurl.stuff)" 62 | /* No ref-style for now 63 | "![image and another,/ image][test]\n\n[test]: http://dsurl.stuff", 64 | "![][test]\n\n[test]: http://dsurl.stuff" 65 | */ 66 | ] 67 | .forEach(function (imageMarkup) { 68 | var processedMarkup = 69 | ghostdown().reduce(function (prev, processor) { 70 | return processor.filter(prev); 71 | }, imageMarkup); 72 | 73 | processedMarkup.should.match(/ 2 | 3 |

    General

    4 |
    5 | 6 |
    7 | 8 | 9 |
    10 |
    11 |
    12 | 13 |
    14 | 15 | 16 |

    The name of your blog

    17 |
    18 | 19 |
    20 | 21 | 22 |

    Describe what your blog is about

    23 |
    24 |
    25 |
    26 | 27 | {{#if logo}} 28 | 29 | {{else}} 30 | 31 | {{/if}} 32 |

    Display a sexy logo for your publication

    33 |
    34 | 35 |
    36 | 37 | {{#if cover}} 38 | cover photo 39 | {{else}} 40 | Upload Image 41 | {{/if}} 42 |

    Display a cover image on your site

    43 |
    44 |
    45 |
    46 | 47 | 48 |

    Address to use for admin notifications

    49 |
    50 | 51 |
    52 | 53 | 54 |

    How many posts should be displayed on each page

    55 |
    56 | 57 |
    58 | 59 | 64 |

    Select a theme for your blog

    65 |
    66 | 67 |
    68 |
    69 |
    -------------------------------------------------------------------------------- /core/test/unit/middleware_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, beforeEach, it*/ 2 | var assert = require('assert'), 3 | should = require('should'), 4 | sinon = require('sinon'), 5 | when = require('when'), 6 | express = require('express'), 7 | middleware = require('../../server/middleware'); 8 | 9 | describe('Middleware', function () { 10 | describe('staticTheme', function () { 11 | var realExpressStatic = express.static; 12 | 13 | beforeEach(function () { 14 | sinon.stub(middleware, 'forwardToExpressStatic').yields(); 15 | }); 16 | 17 | afterEach(function () { 18 | middleware.forwardToExpressStatic.restore(); 19 | }); 20 | 21 | it('should call next if hbs file type', function (done) { 22 | var req = { 23 | url: 'mytemplate.hbs' 24 | }; 25 | 26 | middleware.staticTheme(null)(req, null, function (a) { 27 | should.not.exist(a); 28 | middleware.forwardToExpressStatic.calledOnce.should.be.false; 29 | return done(); 30 | }); 31 | }); 32 | 33 | it('should call next if md file type', function (done) { 34 | var req = { 35 | url: 'README.md' 36 | }; 37 | 38 | middleware.staticTheme(null)(req, null, function (a) { 39 | should.not.exist(a); 40 | middleware.forwardToExpressStatic.calledOnce.should.be.false; 41 | return done(); 42 | }); 43 | }); 44 | 45 | it('should call next if txt file type', function (done) { 46 | var req = { 47 | url: 'LICENSE.txt' 48 | }; 49 | 50 | middleware.staticTheme(null)(req, null, function (a) { 51 | should.not.exist(a); 52 | middleware.forwardToExpressStatic.calledOnce.should.be.false; 53 | return done(); 54 | }); 55 | }); 56 | 57 | it('should call next if json file type', function (done) { 58 | var req = { 59 | url: 'sample.json' 60 | } 61 | 62 | middleware.staticTheme(null)(req, null, function (a) { 63 | should.not.exist(a); 64 | middleware.forwardToExpressStatic.calledOnce.should.be.false; 65 | return done(); 66 | }); 67 | }); 68 | 69 | it('should call express.static if valid file type', function (done) { 70 | var ghostStub = { 71 | paths: function() { 72 | return {activeTheme: 'ACTIVETHEME'}; 73 | } 74 | }; 75 | 76 | var req = { 77 | url: 'myvalidfile.css' 78 | }; 79 | 80 | middleware.staticTheme(ghostStub)(req, null, function (req, res, next) { 81 | middleware.forwardToExpressStatic.calledOnce.should.be.true; 82 | assert.deepEqual(middleware.forwardToExpressStatic.args[0][0], ghostStub); 83 | return done(); 84 | }); 85 | }); 86 | }); 87 | }); 88 | 89 | -------------------------------------------------------------------------------- /core/test/unit/model_roles_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, it, before, beforeEach, afterEach */ 2 | var testUtils = require('./testUtils'), 3 | should = require('should'), 4 | errors = require('../../server/errorHandling'), 5 | 6 | // Stuff we are testing 7 | Models = require('../../server/models'); 8 | 9 | describe("Role Model", function () { 10 | 11 | var RoleModel = Models.Role; 12 | 13 | should.exist(RoleModel); 14 | 15 | before(function (done) { 16 | testUtils.clearData().then(function () { 17 | done(); 18 | }, done); 19 | }); 20 | 21 | beforeEach(function (done) { 22 | this.timeout(5000); 23 | testUtils.initData().then(function () { 24 | done(); 25 | }, done); 26 | }); 27 | 28 | afterEach(function (done) { 29 | testUtils.clearData().then(function () { 30 | done(); 31 | }, done); 32 | }); 33 | 34 | it("can browse roles", function (done) { 35 | RoleModel.browse().then(function (foundRoles) { 36 | should.exist(foundRoles); 37 | 38 | foundRoles.models.length.should.be.above(0); 39 | 40 | done(); 41 | }).then(null, done); 42 | }); 43 | 44 | it("can read roles", function (done) { 45 | RoleModel.read({id: 1}).then(function (foundRole) { 46 | should.exist(foundRole); 47 | 48 | done(); 49 | }).then(null, done); 50 | }); 51 | 52 | it("can edit roles", function (done) { 53 | RoleModel.read({id: 1}).then(function (foundRole) { 54 | should.exist(foundRole); 55 | 56 | return foundRole.set({name: "updated"}).save(); 57 | }).then(function () { 58 | return RoleModel.read({id: 1}); 59 | }).then(function (updatedRole) { 60 | should.exist(updatedRole); 61 | 62 | updatedRole.get("name").should.equal("updated"); 63 | 64 | done(); 65 | }).then(null, done); 66 | }); 67 | 68 | it("can add roles", function (done) { 69 | var newRole = { 70 | name: "test1", 71 | description: "test1 description" 72 | }; 73 | 74 | RoleModel.add(newRole).then(function (createdRole) { 75 | should.exist(createdRole); 76 | 77 | createdRole.attributes.name.should.equal(newRole.name); 78 | createdRole.attributes.description.should.equal(newRole.description); 79 | 80 | done(); 81 | }).then(null, done); 82 | }); 83 | 84 | it("can delete roles", function (done) { 85 | RoleModel.read({id: 1}).then(function (foundRole) { 86 | should.exist(foundRole); 87 | 88 | return RoleModel['delete'](1); 89 | }).then(function () { 90 | return RoleModel.browse(); 91 | }).then(function (foundRoles) { 92 | var hasRemovedId = foundRoles.any(function (role) { 93 | return role.id === 1; 94 | }); 95 | 96 | hasRemovedId.should.equal(false); 97 | 98 | done(); 99 | }).then(null, done); 100 | }); 101 | }); -------------------------------------------------------------------------------- /core/client/assets/vendor/codemirror/mode/gfm/gfm.js: -------------------------------------------------------------------------------- 1 | CodeMirror.defineMode("gfm", function(config) { 2 | var codeDepth = 0; 3 | function blankLine(state) { 4 | state.code = false; 5 | return null; 6 | } 7 | var gfmOverlay = { 8 | startState: function() { 9 | return { 10 | code: false, 11 | codeBlock: false, 12 | ateSpace: false 13 | }; 14 | }, 15 | copyState: function(s) { 16 | return { 17 | code: s.code, 18 | codeBlock: s.codeBlock, 19 | ateSpace: s.ateSpace 20 | }; 21 | }, 22 | token: function(stream, state) { 23 | // Hack to prevent formatting override inside code blocks (block and inline) 24 | if (state.codeBlock) { 25 | if (stream.match(/^```/)) { 26 | state.codeBlock = false; 27 | return null; 28 | } 29 | stream.skipToEnd(); 30 | return null; 31 | } 32 | if (stream.sol()) { 33 | state.code = false; 34 | } 35 | if (stream.sol() && stream.match(/^```/)) { 36 | stream.skipToEnd(); 37 | state.codeBlock = true; 38 | return null; 39 | } 40 | // If this block is changed, it may need to be updated in Markdown mode 41 | if (stream.peek() === '`') { 42 | stream.next(); 43 | var before = stream.pos; 44 | stream.eatWhile('`'); 45 | var difference = 1 + stream.pos - before; 46 | if (!state.code) { 47 | codeDepth = difference; 48 | state.code = true; 49 | } else { 50 | if (difference === codeDepth) { // Must be exact 51 | state.code = false; 52 | } 53 | } 54 | return null; 55 | } else if (state.code) { 56 | stream.next(); 57 | return null; 58 | } 59 | // Check if space. If so, links can be formatted later on 60 | if (stream.eatSpace()) { 61 | state.ateSpace = true; 62 | return null; 63 | } 64 | if (stream.sol() || state.ateSpace) { 65 | state.ateSpace = false; 66 | if(stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/)) { 67 | // User/Project@SHA 68 | // User@SHA 69 | // SHA 70 | return "link"; 71 | } else if (stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/)) { 72 | // User/Project#Num 73 | // User#Num 74 | // #Num 75 | return "link"; 76 | } 77 | } 78 | if (stream.match(/^((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/i)) { 79 | // URLs 80 | // Taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls 81 | // And then (issue #1160) simplified to make it not crash the Chrome Regexp engine 82 | return "link"; 83 | } 84 | stream.next(); 85 | return null; 86 | }, 87 | blankLine: blankLine 88 | }; 89 | CodeMirror.defineMIME("gfmBase", { 90 | name: "markdown", 91 | underscoresBreakWords: false, 92 | taskLists: true, 93 | fencedCodeBlocks: true 94 | }); 95 | return CodeMirror.overlayMode(CodeMirror.getMode(config, "gfmBase"), gfmOverlay); 96 | }, "markdown"); 97 | -------------------------------------------------------------------------------- /core/test/unit/utils/api.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | when = require('when'), 3 | http = require('http'), 4 | HttpMethods, 5 | ApiRouteBase = '/api/v0.1/'; 6 | 7 | HttpMethods = { 8 | GET: 'GET', 9 | POST: 'POST', 10 | PUT: 'PUT', 11 | DELETE: 'DELETE' 12 | }; 13 | 14 | function createRequest(httpMethod, overrides) { 15 | return _.defaults(overrides, { 16 | 'host': 'localhost', 17 | 'port': '2369', 18 | 'method': httpMethod 19 | }); 20 | } 21 | 22 | function post(route, data, authCookie) { 23 | var jsonData = JSON.stringify(data), 24 | options = createRequest(HttpMethods.POST, { 25 | path: route, 26 | headers: { 27 | 'Content-Type': 'application/json', 28 | 'Content-Length': jsonData.length 29 | } 30 | }), 31 | req, 32 | response = '', 33 | deferred = when.defer(); 34 | 35 | if (authCookie) { 36 | options.headers['Cookie'] = authCookie; 37 | } 38 | 39 | req = http.request(options, function (res) { 40 | res.setEncoding('utf-8'); 41 | 42 | if (res.statusCode === 401) { 43 | return deferred.resolver.reject(new Error('401 Unauthorized.')); 44 | } 45 | 46 | res.on('data', function (chunk) { 47 | response += chunk; 48 | }); 49 | 50 | res.on('end', function () { 51 | deferred.resolver.resolve({ 52 | headers: res.headers, 53 | response: JSON.parse(response) 54 | }); 55 | }); 56 | }).on('error', deferred.resolver.reject); 57 | 58 | req.write(jsonData); 59 | req.end(); 60 | 61 | return deferred.promise; 62 | } 63 | 64 | function get(route, authCookie) { 65 | var options = createRequest(HttpMethods.GET, { 66 | path: route, 67 | headers: {} 68 | }), 69 | response = '', 70 | deferred = when.defer(); 71 | 72 | if (authCookie) { 73 | options.headers['Cookie'] = authCookie; 74 | } 75 | 76 | http.get(options, function (res) { 77 | res.setEncoding('utf-8'); 78 | 79 | if (res.statusCode === 401) { 80 | return deferred.resolver.reject(new Error('401 Unauthorized.')); 81 | } 82 | 83 | res.on('data', function (chunk) { 84 | response += chunk; 85 | }); 86 | 87 | res.on('end', function () { 88 | deferred.resolve({ 89 | headers: res.headers, 90 | response: JSON.parse(response) 91 | }); 92 | }); 93 | }).on('error', deferred.resolver.reject); 94 | 95 | return deferred.promise; 96 | } 97 | 98 | function login(email, password) { 99 | var data = { 100 | email: email, 101 | password: password 102 | }; 103 | 104 | return post('/ghost/signin/', data).then(function (response) { 105 | return response.headers['set-cookie']; 106 | }); 107 | } 108 | 109 | module.exports = { 110 | HttpMethods: HttpMethods, 111 | ApiRouteBase: ApiRouteBase, 112 | 113 | createRequest: createRequest, 114 | post: post, 115 | get: get, 116 | 117 | login: login 118 | }; -------------------------------------------------------------------------------- /core/test/unit/model_permissions_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, it, before, beforeEach, afterEach */ 2 | var testUtils = require('./testUtils'), 3 | should = require('should'), 4 | errors = require('../../server/errorHandling'), 5 | 6 | // Stuff we are testing 7 | Models = require('../../server/models'); 8 | 9 | describe("Permission Model", function () { 10 | 11 | var PermissionModel = Models.Permission; 12 | 13 | should.exist(PermissionModel); 14 | 15 | before(function (done) { 16 | testUtils.clearData().then(function () { 17 | done(); 18 | }, done); 19 | }); 20 | 21 | beforeEach(function (done) { 22 | this.timeout(5000); 23 | testUtils.initData().then(function () { 24 | done(); 25 | }, done); 26 | }); 27 | 28 | afterEach(function (done) { 29 | testUtils.clearData().then(function () { 30 | done(); 31 | }, done); 32 | }); 33 | 34 | it("can browse permissions", function (done) { 35 | PermissionModel.browse().then(function (foundPermissions) { 36 | should.exist(foundPermissions); 37 | 38 | foundPermissions.models.length.should.be.above(0); 39 | 40 | done(); 41 | }).then(null, done); 42 | }); 43 | 44 | it("can read permissions", function (done) { 45 | PermissionModel.read({id: 1}).then(function (foundPermission) { 46 | should.exist(foundPermission); 47 | 48 | done(); 49 | }).then(null, done); 50 | }); 51 | 52 | it("can edit permissions", function (done) { 53 | PermissionModel.read({id: 1}).then(function (foundPermission) { 54 | should.exist(foundPermission); 55 | 56 | return foundPermission.set({name: "updated"}).save(); 57 | }).then(function () { 58 | return PermissionModel.read({id: 1}); 59 | }).then(function (updatedPermission) { 60 | should.exist(updatedPermission); 61 | 62 | updatedPermission.get("name").should.equal("updated"); 63 | 64 | done(); 65 | }).then(null, done); 66 | }); 67 | 68 | it("can add permissions", function (done) { 69 | var newPerm = { 70 | name: "testperm1", 71 | object_type: 'test', 72 | action_type: 'test' 73 | }; 74 | 75 | PermissionModel.add(newPerm).then(function (createdPerm) { 76 | should.exist(createdPerm); 77 | 78 | createdPerm.attributes.name.should.equal(newPerm.name); 79 | 80 | done(); 81 | }).then(null, done); 82 | }); 83 | 84 | it("can delete permissions", function (done) { 85 | PermissionModel.read({id: 1}).then(function (foundPermission) { 86 | should.exist(foundPermission); 87 | 88 | return PermissionModel['delete'](1); 89 | }).then(function () { 90 | return PermissionModel.browse(); 91 | }).then(function (foundPermissions) { 92 | var hasRemovedId = foundPermissions.any(function (permission) { 93 | return permission.id === 1; 94 | }); 95 | 96 | hasRemovedId.should.equal(false); 97 | 98 | done(); 99 | }).then(null, done); 100 | }); 101 | }); -------------------------------------------------------------------------------- /config.example.js: -------------------------------------------------------------------------------- 1 | // # Ghost Configuration 2 | // Setup your Ghost install for various environments 3 | 4 | var path = require('path'), 5 | config; 6 | 7 | config = { 8 | // ### Development **(default)** 9 | development: { 10 | // The url to use when providing links to the site, E.g. in RSS and email. 11 | url: 'http://my-ghost-blog.com', 12 | 13 | // Example mail config 14 | // Visit http://docs.ghost.org/mail for instructions 15 | // ``` 16 | // mail: { 17 | // transport: 'SMTP', 18 | // options: { 19 | // service: 'Mailgun', 20 | // auth: { 21 | // user: '', // mailgun username 22 | // pass: '' // mailgun password 23 | // } 24 | // } 25 | // }, 26 | // ``` 27 | 28 | database: { 29 | client: 'sqlite3', 30 | connection: { 31 | filename: path.join(__dirname, '/content/data/ghost-dev.db') 32 | }, 33 | debug: false 34 | }, 35 | server: { 36 | // Host to be passed to node's `net.Server#listen()` 37 | host: '127.0.0.1', 38 | // Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT` 39 | port: '2368' 40 | } 41 | }, 42 | 43 | // ### Production 44 | // When running Ghost in the wild, use the production environment 45 | // Configure your URL and mail settings here 46 | production: { 47 | url: 'http://my-ghost-blog.com', 48 | mail: {}, 49 | database: { 50 | client: 'sqlite3', 51 | connection: { 52 | filename: path.join(__dirname, '/content/data/ghost.db') 53 | }, 54 | debug: false 55 | }, 56 | server: { 57 | // Host to be passed to node's `net.Server#listen()` 58 | host: '127.0.0.1', 59 | // Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT` 60 | port: '2368' 61 | } 62 | }, 63 | 64 | // **Developers only need to edit below here** 65 | 66 | // ### Testing 67 | // Used when developing Ghost to run tests and check the health of Ghost 68 | // Uses a different port number 69 | testing: { 70 | url: 'http://127.0.0.1:2369', 71 | database: { 72 | client: 'sqlite3', 73 | connection: { 74 | filename: path.join(__dirname, '/content/data/ghost-test.db') 75 | } 76 | }, 77 | server: { 78 | host: '127.0.0.1', 79 | port: '2369' 80 | } 81 | }, 82 | 83 | // ### Travis 84 | // Automated testing run through Github 85 | travis: { 86 | url: 'http://127.0.0.1:2368', 87 | database: { 88 | client: 'sqlite3', 89 | connection: { 90 | filename: path.join(__dirname, '/content/data/ghost-travis.db') 91 | } 92 | }, 93 | server: { 94 | host: '127.0.0.1', 95 | port: '2368' 96 | } 97 | } 98 | }; 99 | 100 | // Export config 101 | module.exports = config; 102 | -------------------------------------------------------------------------------- /core/test/functional/admin/content_test.js: -------------------------------------------------------------------------------- 1 | /*globals casper, __utils__, url, testPost */ 2 | 3 | CasperTest.begin("Content screen is correct", 20, function suite(test) { 4 | // Create a sample post 5 | casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() { 6 | test.assertTitle('Ghost Admin', 'Ghost admin has no title'); 7 | }); 8 | 9 | casper.then(function createTestPost() { 10 | casper.sendKeys('#entry-title', testPost.title); 11 | casper.writeContentToCodeMirror(testPost.html); 12 | }); 13 | 14 | casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() { 15 | test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct'); 16 | }); 17 | 18 | casper.thenClick('.js-publish-button'); 19 | 20 | casper.waitForResource(/posts/, function checkPostWasCreated() { 21 | test.assertExists('.notification-success', 'got success notification'); 22 | }); 23 | 24 | // Begin test 25 | casper.thenOpen(url + "ghost/content/", function testTitleAndUrl() { 26 | test.assertTitle("Ghost Admin", "Ghost admin has no title"); 27 | test.assertUrlMatch(/ghost\/content\/$/, "Ghost doesn't require login this time"); 28 | }); 29 | 30 | casper.then(function testMenus() { 31 | test.assertExists("#main-menu", "Main menu is present"); 32 | test.assertSelectorHasText("#main-menu .content a", "Content"); 33 | test.assertSelectorHasText("#main-menu .editor a", "New Post"); 34 | test.assertSelectorHasText("#main-menu .settings a", "Settings"); 35 | 36 | test.assertExists("#usermenu", "User menu is present"); 37 | test.assertSelectorHasText("#usermenu .usermenu-profile a", "Your Profile"); 38 | test.assertSelectorHasText("#usermenu .usermenu-help a", "Help / Support"); 39 | test.assertSelectorHasText("#usermenu .usermenu-signout a", "Sign Out"); 40 | }); 41 | 42 | casper.then(function testViews() { 43 | test.assertExists(".content-view-container", "Content main view is present"); 44 | test.assertExists(".content-list-content", "Content list view is present"); 45 | test.assertExists(".content-list-content li .entry-title", "Content list view has at least one item"); 46 | test.assertExists(".content-preview", "Content preview is present"); 47 | test.assertSelectorHasText(".content-list-content li:first-child h3", testPost.title, "item is present and has content"); 48 | }); 49 | 50 | casper.then(function testActiveItem() { 51 | test.assertEvalEquals(function () { 52 | return document.querySelector('.content-list-content li').className; 53 | }, "active", "first item is active"); 54 | 55 | }).thenClick(".content-list-content li:nth-child(2) a", function then() { 56 | test.assertEvalEquals(function () { 57 | return document.querySelectorAll('.content-list-content li')[1].className; 58 | }, "active", "second item is active"); 59 | }); 60 | }); 61 | 62 | CasperTest.begin('Infinite scrolling', 1, function suite(test) { 63 | // Placeholder for infinite scrolling/pagination tests (will need to setup 16+ posts). 64 | 65 | casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() { 66 | test.assertTitle('Ghost Admin', 'Ghost admin has no title'); 67 | }); 68 | }); -------------------------------------------------------------------------------- /core/test/unit/errorHandling_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, beforeEach, it*/ 2 | var testUtils = require('./testUtils'), 3 | should = require('should'), 4 | when = require('when'), 5 | sinon = require('sinon'), 6 | 7 | // Stuff we are testing 8 | colors = require("colors"), 9 | errors = require('../../server/errorHandling'), 10 | // storing current environment 11 | currentEnv = process.env.NODE_ENV; 12 | 13 | describe("Error handling", function () { 14 | 15 | // Just getting rid of jslint unused error 16 | should.exist(errors); 17 | 18 | it("throws error objects", function () { 19 | var toThrow = new Error("test1"), 20 | runThrowError = function () { 21 | errors.throwError(toThrow); 22 | }; 23 | 24 | runThrowError.should['throw']("test1"); 25 | }); 26 | 27 | it("throws error strings", function () { 28 | var toThrow = "test2", 29 | runThrowError = function () { 30 | errors.throwError(toThrow); 31 | }; 32 | 33 | runThrowError.should['throw']("test2"); 34 | }); 35 | 36 | it("throws error even if nothing passed", function () { 37 | var runThrowError = function () { 38 | errors.throwError(); 39 | }; 40 | 41 | runThrowError.should['throw']("An error occurred"); 42 | }); 43 | 44 | it("logs errors", function () { 45 | var err = new Error("test1"), 46 | logStub = sinon.stub(console, "error"); 47 | 48 | // give environment a value that will console log 49 | process.env.NODE_ENV = "development"; 50 | errors.logError(err); 51 | 52 | // Calls log with message on Error objects 53 | logStub.calledWith("\nERROR:".red, err.message.red).should.equal(true); 54 | 55 | logStub.reset(); 56 | 57 | err = "test2"; 58 | 59 | errors.logError(err); 60 | 61 | // Calls log with string on strings 62 | logStub.calledWith("\nERROR:".red, err.red).should.equal(true); 63 | 64 | logStub.restore(); 65 | process.env.NODE_ENV = currentEnv; 66 | 67 | }); 68 | 69 | it("logs promise errors and redirects", function (done) { 70 | var def = when.defer(), 71 | prom = def.promise, 72 | req = null, 73 | res = { 74 | redirect: function () { 75 | return; 76 | } 77 | }, 78 | logStub = sinon.stub(console, "error"), 79 | redirectStub = sinon.stub(res, "redirect"); 80 | 81 | // give environment a value that will console log 82 | process.env.NODE_ENV = "development"; 83 | prom.then(function () { 84 | throw new Error("Ran success handler"); 85 | }, errors.logErrorWithRedirect("test1", null, null, "/testurl", req, res)); 86 | 87 | prom.otherwise(function () { 88 | logStub.calledWith("\nERROR:".red, "test1".red).should.equal(true); 89 | logStub.restore(); 90 | 91 | redirectStub.calledWith('/testurl').should.equal(true); 92 | redirectStub.restore(); 93 | 94 | done(); 95 | }); 96 | prom.ensure(function () { 97 | // gives the environment the correct value back 98 | process.env.NODE_ENV = currentEnv; 99 | }); 100 | def.reject(); 101 | }); 102 | }); -------------------------------------------------------------------------------- /core/client/tpl/settings/user-profile.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 |

    Your Profile

    4 |
    5 | 6 |
    7 |
    8 | 9 |
    10 | 11 | 15 | 16 | 87 |
    88 | -------------------------------------------------------------------------------- /core/client/assets/vendor/icheck/jquery.icheck.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * iCheck v0.8.5 jQuery plugin, http://git.io/uhUPMA 3 | */ 4 | (function(f,m,z,u,k,r,l,n,D,t,v,s){function A(a,c,j){var d=a[0],b=/ble/.test(j)?r:k;active="update"==j?{checked:d[k],disabled:d[r]}:d[b];if(/^ch|di/.test(j)&&!active)w(a,b);else if(/^un|en/.test(j)&&active)x(a,b);else if("update"==j)for(var b in active)active[b]?w(a,b,!0):x(a,b,!0);else if(!c||"toggle"==j)c||a.trigger("ifClicked"),active?d[l]!==u&&x(a,b):w(a,b)}function w(a,c,j){var d=a[0],b=a.parent(),E=c==r?"enabled":"un"+k,n=e(a,E+g(d[l])),h=e(a,c+g(d[l]));if(!0!==d[c]&&!j&&(d[c]=!0,a.trigger("ifChanged").trigger("if"+ 5 | g(c)),c==k&&d[l]==u&&d.name)){j=a.closest("form");var p='input[name="'+d.name+'"]',p=j.length?j.find(p):f(p);p.each(function(){this!==d&&f(this).data(m)&&x(f(this),c)})}d[r]&&e(a,s,!0)&&b.find("."+m+"-helper").css(s,"default");b[t](h||e(a,c));b[v](n||e(a,E)||"")}function x(a,c,j){var d=a[0],b=a.parent(),f=c==r?"enabled":"un"+k,n=e(a,f+g(d[l])),h=e(a,c+g(d[l]));!1!==d[c]&&!j&&(d[c]=!1,a.trigger("ifChanged").trigger("if"+g(f)));!d[r]&&e(a,s,!0)&&b.find("."+m+"-helper").css(s,"pointer");b[v](h||e(a, 6 | c)||"");b[t](n||e(a,f))}function F(a,c){a.data(m)&&(a.parent().html(a.attr("style",a.data(m).s||"").trigger(c||"")),a.off(".i").unwrap(),f('label[for="'+a[0].id+'"]').add(a.closest("label")).off(".i"))}function e(a,c,f){if(a.data(m))return a.data(m).o[c+(f?"":"Class")]}function g(a){return a.charAt(0).toUpperCase()+a.slice(1)}f.fn[m]=function(a,c){var j=navigator.userAgent,d=/ipad|iphone|ipod/i.test(j),b=":"+z+", :"+u,e=f(),g=function(a){a.each(function(){var a=f(this);e=a.is(b)?e.add(a):e.add(a.find(b))})}; 7 | if(/^(check|uncheck|toggle|disable|enable|update|destroy)$/.test(a))return g(this),e.each(function(){var d=f(this);"destroy"==a?F(d,"ifDestroyed"):A(d,!0,a);f.isFunction(c)&&c()});if("object"==typeof a||!a){var h=f.extend({checkedClass:k,disabledClass:r,labelHover:!0},a),p=h.handle,s=h.hoverClass||"hover",I=h.focusClass||"focus",G=h.activeClass||"active",H=!!h.labelHover,C=h.labelHoverClass||"hover",y=(""+h.increaseArea).replace("%","")|0;if(p==z||p==u)b=":"+p;-50>y&&(y=-50);g(this);return e.each(function(){var a= 8 | f(this);F(a);var c=this,b=c.id,e=-y+"%",g=100+2*y+"%",g={position:"absolute",top:e,left:e,display:"block",width:g,height:g,margin:0,padding:0,background:"#fff",border:0,opacity:0},e=d||/android|blackberry|windows phone|opera mini/i.test(j)?{position:"absolute",visibility:"hidden"}:y?g:{position:"absolute",opacity:0},p=c[l]==z?h.checkboxClass||"i"+z:h.radioClass||"i"+u,B=f('label[for="'+b+'"]').add(a.closest("label")),q=a.wrap('
    ').trigger("ifCreated").parent().append(h.insert), 9 | g=f('').css(g).appendTo(q);a.data(m,{o:h,s:a.attr("style")}).css(e);h.inheritClass&&q[t](c.className);h.inheritID&&b&&q.attr("id",m+"-"+b);"static"==q.css("position")&&q.css("position","relative");A(a,!0,"update");if(B.length)B.on(n+".i mouseenter.i mouseleave.i "+D,function(b){var e=b[l],g=f(this);if(!c[r])if(e==n?A(a,!1,!0):H&&(/ve|nd/.test(e)?(q[v](s),g[v](C)):(q[t](s),g[t](C))),d)b.stopPropagation();else return!1});a.on(n+".i focus.i blur.i keyup.i keydown.i keypress.i", 10 | function(b){var d=b[l];b=b.keyCode;if(d==n)return!1;if("keydown"==d&&32==b)return c[l]==u&&c[k]||(c[k]?x(a,k):w(a,k)),!1;if("keyup"==d&&c[l]==u)!c[k]&&w(a,k);else if(/us|ur/.test(d))q["blur"==d?v:t](I)});g.on(n+" mousedown mouseup mouseover mouseout "+D,function(b){var e=b[l],f=/wn|up/.test(e)?G:s;if(!c[r]){if(e==n)A(a,!1,!0);else{if(/wn|er|in/.test(e))q[t](f);else q[v](f+" "+G);if(B.length&&H&&f==s)B[/ut|nd/.test(e)?v:t](C)}if(d)b.stopPropagation();else return!1}})})}return this}})(jQuery,"iCheck", 11 | "checkbox","radio","checked","disabled","type","click","touchbegin.i touchend.i","addClass","removeClass","cursor"); -------------------------------------------------------------------------------- /core/server/views/editor.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 | {{! TODO: Add "scrolling" class only when one of the panels is scrolled down by 5px or more }} 3 |
    4 |
    5 |
    6 | 9 |
    10 |
    11 | 12 |
    13 |
    14 | Markdown 15 | 16 |
    17 |
    18 | 19 |
    20 |
    {{!.entry-markdown}} 21 | 22 |
    23 |
    24 | Preview 0 words 25 |
    26 |
    27 |
    28 | {{!The content gets inserted in here, bitches!}} 29 |
    30 |
    31 |
    {{!.entry-preview}} 32 |
    33 |
    34 | 77 |
    78 | -------------------------------------------------------------------------------- /core/client/tpl/modals/markdown.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | For further Markdown syntax reference: Markdown Documentation 114 |
    115 | -------------------------------------------------------------------------------- /core/test/functional/admin/logout_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests logging out and attempting to sign up 3 | */ 4 | 5 | /*globals casper, __utils__, url, testPost, falseUser, email */ 6 | CasperTest.begin("Ghost logout works correctly", 2, function suite(test) { 7 | CasperTest.Routines.register.run(test); 8 | CasperTest.Routines.logout.run(test); 9 | CasperTest.Routines.login.run(test); 10 | 11 | casper.thenOpen(url + "ghost/", function then() { 12 | test.assertEquals(casper.getCurrentUrl(), url + "ghost/", "Ghost doesn't require login this time"); 13 | }); 14 | 15 | casper.thenClick('#usermenu a').waitFor(function checkOpaque() { 16 | return this.evaluate(function () { 17 | var loginBox = document.querySelector('#usermenu .overlay.open'); 18 | return window.getComputedStyle(loginBox).getPropertyValue('display') === "block" 19 | && window.getComputedStyle(loginBox).getPropertyValue('opacity') === "1"; 20 | }); 21 | }); 22 | 23 | casper.thenClick('.usermenu-signout a'); 24 | casper.waitForResource(/signin/); 25 | 26 | casper.waitForSelector('.notification-success', function onSuccess() { 27 | test.assert(true, 'Got success notification'); 28 | }, function onTimeout() { 29 | test.assert(false, 'No success notification :('); 30 | }); 31 | }, true); 32 | 33 | // has to be done after signing out 34 | CasperTest.begin("Can't spam signin", 3, function suite(test) { 35 | casper.thenOpen(url + "ghost/signin/", function testTitle() { 36 | test.assertTitle("Ghost Admin", "Ghost admin has no title"); 37 | }); 38 | 39 | casper.waitFor(function checkOpaque() { 40 | return this.evaluate(function () { 41 | var loginBox = document.querySelector('.login-box'); 42 | return window.getComputedStyle(loginBox).getPropertyValue('display') === "table" 43 | && window.getComputedStyle(loginBox).getPropertyValue('opacity') === "1"; 44 | }); 45 | }, function then() { 46 | this.fill("#login", falseUser, true); 47 | casper.wait(200, function doneWait() { 48 | this.fill("#login", falseUser, true); 49 | }); 50 | 51 | }); 52 | 53 | casper.waitForSelector('.notification-error', function onSuccess() { 54 | test.assert(true, 'Got error notification'); 55 | test.assertSelectorDoesntHaveText('.notification-error', '[object Object]'); 56 | }, function onTimeout() { 57 | test.assert(false, 'No error notification :('); 58 | }); 59 | }, true); 60 | 61 | CasperTest.begin("Ghost signup fails properly", 5, function suite(test) { 62 | casper.thenOpen(url + "ghost/signup/", function then() { 63 | test.assertEquals(casper.getCurrentUrl(), url + "ghost/signup/", "Reached signup page"); 64 | }); 65 | 66 | casper.then(function signupWithShortPassword() { 67 | this.fill("#signup", {email: email, password: 'test'}, true); 68 | }); 69 | 70 | // should now throw a short password error 71 | casper.waitForResource(/signup/); 72 | casper.waitForSelector('.notification-error', function onSuccess() { 73 | test.assert(true, 'Got error notification'); 74 | test.assertSelectorDoesntHaveText('.notification-error', '[object Object]'); 75 | }, function onTimeout() { 76 | test.assert(false, 'No error notification :('); 77 | }); 78 | 79 | casper.then(function signupWithLongPassword() { 80 | this.fill("#signup", {email: email, password: 'testing1234'}, true); 81 | }); 82 | 83 | // should now throw a 1 user only error 84 | casper.waitForResource(/signup/); 85 | casper.waitForSelector('.notification-error', function onSuccess() { 86 | test.assert(true, 'Got error notification'); 87 | test.assertSelectorDoesntHaveText('.notification-error', '[object Object]'); 88 | }, function onTimeout() { 89 | test.assert(false, 'No error notification :('); 90 | }); 91 | }, true); -------------------------------------------------------------------------------- /core/test/unit/import_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, beforeEach, it*/ 2 | var testUtils = require('./testUtils'), 3 | should = require('should'), 4 | sinon = require('sinon'), 5 | when = require('when'), 6 | _ = require("underscore"), 7 | errors = require('../../server/errorHandling'), 8 | 9 | // Stuff we are testing 10 | knex = require("../../server/models/base").Knex, 11 | migration = require('../../server/data/migration'), 12 | exporter = require('../../server/data/export'), 13 | importer = require('../../server/data/import'), 14 | Importer000 = require('../../server/data/import/000'), 15 | fixtures = require('../../server/data/fixtures'), 16 | Settings = require('../../server/models/settings').Settings; 17 | 18 | describe("Import", function () { 19 | 20 | should.exist(exporter); 21 | should.exist(importer); 22 | 23 | beforeEach(function (done) { 24 | // clear database... we need to initialise it manually for each test 25 | testUtils.clearData().then(function () { 26 | done(); 27 | }, done); 28 | }); 29 | 30 | it("resolves 000", function (done) { 31 | var importStub = sinon.stub(Importer000, "importData", function () { 32 | return when.resolve(); 33 | }), 34 | fakeData = { test: true }; 35 | 36 | importer("000", fakeData).then(function () { 37 | importStub.calledWith(fakeData).should.equal(true); 38 | 39 | importStub.restore(); 40 | 41 | done(); 42 | }).then(null, done); 43 | }); 44 | 45 | describe("000", function () { 46 | this.timeout(4000); 47 | 48 | should.exist(Importer000); 49 | 50 | it("imports data from 000", function (done) { 51 | var exportData; 52 | 53 | // initialise database to version 000 - confusingly we have to set the max version to be one higher 54 | // than the migration version we want. Could just use migrate from fresh here... but this is more explicit 55 | migration.migrateUpFromVersion('000', '001').then(function () { 56 | // Load the fixtures 57 | return fixtures.populateFixtures(); 58 | }).then(function () { 59 | // Initialise the default settings 60 | return Settings.populateDefaults(); 61 | }).then(function () { 62 | // export the version 000 data ready to import 63 | // TODO: Should have static test data here? 64 | return exporter(); 65 | }).then(function (exported) { 66 | exportData = exported; 67 | 68 | return importer("000", exportData); 69 | }).then(function () { 70 | // Grab the data from tables 71 | return when.all([ 72 | knex("users").select(), 73 | knex("posts").select(), 74 | knex("settings").select(), 75 | knex("tags").select() 76 | ]); 77 | }).then(function (importedData) { 78 | 79 | should.exist(importedData); 80 | importedData.length.should.equal(4, 'Did not get data successfully'); 81 | 82 | // we always have 0 users as there isn't one in fixtures 83 | importedData[0].length.should.equal(0, 'There should not be a user'); 84 | // import no longer requires all data to be dropped, and adds posts 85 | importedData[1].length.should.equal(exportData.data.posts.length + 1, 'Wrong number of posts'); 86 | 87 | // test settings 88 | importedData[2].length.should.be.above(0, 'Wrong number of settings'); 89 | _.findWhere(importedData[2], {key: "databaseVersion"}).value.should.equal("000", 'Wrong database version'); 90 | 91 | // test tags 92 | importedData[3].length.should.equal(exportData.data.tags.length, 'no new tags'); 93 | 94 | done(); 95 | }).then(null, done); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /core/client/assets/vendor/codemirror/mode/gfm/test.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var mode = CodeMirror.getMode({tabSize: 4}, "gfm"); 3 | function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); } 4 | 5 | MT("emInWordAsterisk", 6 | "foo[em *bar*]hello"); 7 | 8 | MT("emInWordUnderscore", 9 | "foo_bar_hello"); 10 | 11 | MT("emStrongUnderscore", 12 | "[strong __][em&strong _foo__][em _] bar"); 13 | 14 | MT("fencedCodeBlocks", 15 | "[comment ```]", 16 | "[comment foo]", 17 | "", 18 | "[comment ```]", 19 | "bar"); 20 | 21 | MT("fencedCodeBlockModeSwitching", 22 | "[comment ```javascript]", 23 | "[variable foo]", 24 | "", 25 | "[comment ```]", 26 | "bar"); 27 | 28 | MT("taskListAsterisk", 29 | "[variable-2 * []] foo]", // Invalid; must have space or x between [] 30 | "[variable-2 * [ ]]bar]", // Invalid; must have space after ] 31 | "[variable-2 * [x]]hello]", // Invalid; must have space after ] 32 | "[variable-2 * ][meta [ ]]][variable-2 [world]]]", // Valid; tests reference style links 33 | " [variable-3 * ][property [x]]][variable-3 foo]"); // Valid; can be nested 34 | 35 | MT("taskListPlus", 36 | "[variable-2 + []] foo]", // Invalid; must have space or x between [] 37 | "[variable-2 + [ ]]bar]", // Invalid; must have space after ] 38 | "[variable-2 + [x]]hello]", // Invalid; must have space after ] 39 | "[variable-2 + ][meta [ ]]][variable-2 [world]]]", // Valid; tests reference style links 40 | " [variable-3 + ][property [x]]][variable-3 foo]"); // Valid; can be nested 41 | 42 | MT("taskListDash", 43 | "[variable-2 - []] foo]", // Invalid; must have space or x between [] 44 | "[variable-2 - [ ]]bar]", // Invalid; must have space after ] 45 | "[variable-2 - [x]]hello]", // Invalid; must have space after ] 46 | "[variable-2 - ][meta [ ]]][variable-2 [world]]]", // Valid; tests reference style links 47 | " [variable-3 - ][property [x]]][variable-3 foo]"); // Valid; can be nested 48 | 49 | MT("taskListNumber", 50 | "[variable-2 1. []] foo]", // Invalid; must have space or x between [] 51 | "[variable-2 2. [ ]]bar]", // Invalid; must have space after ] 52 | "[variable-2 3. [x]]hello]", // Invalid; must have space after ] 53 | "[variable-2 4. ][meta [ ]]][variable-2 [world]]]", // Valid; tests reference style links 54 | " [variable-3 1. ][property [x]]][variable-3 foo]"); // Valid; can be nested 55 | 56 | MT("SHA", 57 | "foo [link be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2] bar"); 58 | 59 | MT("shortSHA", 60 | "foo [link be6a8cc] bar"); 61 | 62 | MT("tooShortSHA", 63 | "foo be6a8c bar"); 64 | 65 | MT("longSHA", 66 | "foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd22 bar"); 67 | 68 | MT("badSHA", 69 | "foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cg2 bar"); 70 | 71 | MT("userSHA", 72 | "foo [link bar@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2] hello"); 73 | 74 | MT("userProjectSHA", 75 | "foo [link bar/hello@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2] world"); 76 | 77 | MT("num", 78 | "foo [link #1] bar"); 79 | 80 | MT("badNum", 81 | "foo #1bar hello"); 82 | 83 | MT("userNum", 84 | "foo [link bar#1] hello"); 85 | 86 | MT("userProjectNum", 87 | "foo [link bar/hello#1] world"); 88 | 89 | MT("vanillaLink", 90 | "foo [link http://www.example.com/] bar"); 91 | 92 | MT("vanillaLinkPunctuation", 93 | "foo [link http://www.example.com/]. bar"); 94 | 95 | MT("vanillaLinkExtension", 96 | "foo [link http://www.example.com/index.html] bar"); 97 | 98 | MT("notALink", 99 | "[comment ```css]", 100 | "[tag foo] {[property color][operator :][keyword black];}", 101 | "[comment ```][link http://www.example.com/]"); 102 | 103 | MT("notALink", 104 | "[comment ``foo `bar` http://www.example.com/``] hello"); 105 | 106 | MT("notALink", 107 | "[comment `foo]", 108 | "[link http://www.example.com/]", 109 | "[comment `foo]", 110 | "", 111 | "[link http://www.example.com/]"); 112 | })(); 113 | -------------------------------------------------------------------------------- /core/config-loader.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | url = require('url'), 3 | when = require('when'), 4 | errors = require('./server/errorHandling'), 5 | path = require('path'), 6 | 7 | appRoot = path.resolve(__dirname, '../'), 8 | configexample = path.join(appRoot, 'config.example.js'), 9 | config = path.join(appRoot, 'config.js'); 10 | 11 | function writeConfigFile() { 12 | var written = when.defer(); 13 | 14 | /* Check for config file and copy from config.example.js 15 | if one doesn't exist. After that, start the server. */ 16 | fs.exists(configexample, function checkTemplate(templateExists) { 17 | var read, 18 | write; 19 | 20 | if (!templateExists) { 21 | return errors.logError(new Error('Could not locate a configuration file.'), appRoot, 'Please check your deployment for config.js or config.example.js.'); 22 | } 23 | 24 | // Copy config.example.js => config.js 25 | read = fs.createReadStream(configexample); 26 | read.on('error', function (err) { 27 | return errors.logError(new Error('Could not open config.example.js for read.'), appRoot, 'Please check your deployment for config.js or config.example.js.'); 28 | }); 29 | read.on('end', written.resolve); 30 | 31 | write = fs.createWriteStream(config); 32 | write.on('error', function (err) { 33 | return errors.logError(new Error('Could not open config.js for write.'), appRoot, 'Please check your deployment for config.js or config.example.js.'); 34 | }); 35 | 36 | read.pipe(write); 37 | }); 38 | 39 | return written.promise; 40 | } 41 | 42 | function validateConfigEnvironment() { 43 | var envVal = process.env.NODE_ENV || 'undefined', 44 | hasHostAndPort, 45 | hasSocket, 46 | config, 47 | parsedUrl; 48 | 49 | try { 50 | config = require('../config')[envVal]; 51 | } catch (ignore) { 52 | 53 | } 54 | 55 | 56 | // Check if we don't even have a config 57 | if (!config) { 58 | errors.logError(new Error('Cannot find the configuration for the current NODE_ENV'), "NODE_ENV=" + envVal, 'Ensure your config.js has a section for the current NODE_ENV value'); 59 | return when.reject(); 60 | } 61 | 62 | // Check that our url is valid 63 | parsedUrl = url.parse(config.url || 'invalid', false, true); 64 | if (!parsedUrl.host) { 65 | errors.logError(new Error('Your site url in config.js is invalid.'), config.url, 'Please make sure this is a valid url before restarting'); 66 | return when.reject(); 67 | } 68 | 69 | // Check that we have database values 70 | if (!config.database) { 71 | errors.logError(new Error('Your database configuration in config.js is invalid.'), JSON.stringify(config.database), 'Please make sure this is a valid Bookshelf database configuration'); 72 | return when.reject(); 73 | } 74 | 75 | hasHostAndPort = config.server && !!config.server.host && !!config.server.port; 76 | hasSocket = config.server && !!config.server.socket; 77 | 78 | // Check for valid server host and port values 79 | if (!config.server || !(hasHostAndPort || hasSocket)) { 80 | errors.logError(new Error('Your server values (socket, or host and port) in config.js are invalid.'), JSON.stringify(config.server), 'Please provide them before restarting.'); 81 | return when.reject(); 82 | } 83 | 84 | return when.resolve(); 85 | } 86 | 87 | exports.loadConfig = function () { 88 | var loaded = when.defer(); 89 | /* Check for config file and copy from config.example.js 90 | if one doesn't exist. After that, start the server. */ 91 | fs.exists(config, function checkConfig(configExists) { 92 | if (configExists) { 93 | validateConfigEnvironment().then(loaded.resolve).otherwise(loaded.reject); 94 | } else { 95 | writeConfigFile().then(validateConfigEnvironment).then(loaded.resolve).otherwise(loaded.reject); 96 | } 97 | }); 98 | return loaded.promise; 99 | }; 100 | -------------------------------------------------------------------------------- /core/test/functional/admin/login_test.js: -------------------------------------------------------------------------------- 1 | /*globals casper, __utils__, url, user, falseUser */ 2 | 3 | CasperTest.begin('Ensure Session is Killed', 1, function suite(test) { 4 | casper.thenOpen(url + 'logout/', function (response) { 5 | test.assertUrlMatch(/ghost\/sign/, 'We got redirected to signin or signup page'); 6 | }); 7 | }, true); 8 | 9 | CasperTest.begin('Ensure a User is Registered', 2, function suite(test) { 10 | casper.thenOpen(url + 'ghost/signup/'); 11 | 12 | casper.waitForOpaque(".signup-box", 13 | function then() { 14 | this.fill("#signup", newUser, true); 15 | }, 16 | function onTimeout() { 17 | test.fail('Sign up form didn\'t fade in.'); 18 | }); 19 | 20 | casper.waitForSelectorTextChange('.notification-error', function onSuccess() { 21 | test.assertSelectorHasText('.notification-error', 'already registered'); 22 | // If the previous assert succeeds, then we should skip the next check and just pass. 23 | casper.echo('Already registered!'); 24 | }, function onTimeout() { 25 | test.assertUrlMatch(/\/ghost\/$/, 'If we\'re not already registered, we should be logged in.'); 26 | casper.echo('Successfully registered.'); 27 | }, 2000); 28 | 29 | casper.thenOpen(url + 'logout/', function then() { 30 | test.assertUrlMatch(/ghost\/signin/, 'We got redirected to signin page.'); 31 | }); 32 | }, true); 33 | 34 | CasperTest.begin("Ghost admin will load login page", 2, function suite(test) { 35 | casper.thenOpen(url + "ghost", function testTitleAndUrl() { 36 | test.assertTitle("Ghost Admin", "Ghost admin has no title"); 37 | test.assertUrlMatch(/ghost\/signin\/$/, 'We should be presented with the signin page.'); 38 | }); 39 | }, true); 40 | 41 | CasperTest.begin('Redirects login to signin', 2, function suite(test) { 42 | casper.start(url + 'ghost/login/', function testRedirect(response) { 43 | test.assertEqual(response.status, 200, 'Response status should be 200.'); 44 | test.assertUrlMatch(/ghost\/signin\/$/, 'Should be redirected to /signin/.'); 45 | }); 46 | }, true); 47 | 48 | CasperTest.begin("Can't spam it", 4, function suite(test) { 49 | casper.thenOpen(url + "ghost/signin/", function testTitle() { 50 | test.assertTitle("Ghost Admin", "Ghost admin has no title"); 51 | }); 52 | 53 | casper.waitForOpaque(".login-box", 54 | function then() { 55 | this.fill("#login", falseUser, true); 56 | }, 57 | function onTimeout() { 58 | test.fail('Sign in form didn\'t fade in.'); 59 | }); 60 | 61 | casper.wait(200, function doneWait() { 62 | this.fill("#login", falseUser, true); 63 | }); 64 | 65 | casper.waitForSelector('.notification-error', function onSuccess() { 66 | test.assert(true, 'Save without title results in error notification as expected'); 67 | test.assertSelectorDoesntHaveText('.notification-error', '[object Object]'); 68 | test.assertSelectorHasText('.notification-error', 'Slow down, there are way too many login attempts!'); 69 | }, function onTimeout() { 70 | test.assert(false, 'Spamming the login did not result in an error notification'); 71 | }); 72 | 73 | // This test causes the spam notification 74 | // add a wait to ensure future tests don't get tripped up by this. 75 | casper.wait(1000); 76 | }, true); 77 | 78 | CasperTest.begin("Can login to Ghost", 4, function suite(test) { 79 | casper.thenOpen(url + "ghost/login/", function testTitle() { 80 | test.assertTitle("Ghost Admin", "Ghost admin has no title"); 81 | }); 82 | 83 | casper.waitForOpaque(".login-box", 84 | function then() { 85 | this.fill("#login", user, true); 86 | }); 87 | 88 | casper.waitForResource(/ghost\/$/, function testForDashboard() { 89 | test.assertUrlMatch(/ghost\/$/, 'We got redirected to the Ghost page'); 90 | test.assertExists("#global-header", "Global admin header is present"); 91 | test.assertExists(".manage", "We're now on content"); 92 | }, function onTimeOut() { 93 | test.fail('Failed to load ghost/ resource'); 94 | }); 95 | }, true); 96 | -------------------------------------------------------------------------------- /core/server/mail.js: -------------------------------------------------------------------------------- 1 | var cp = require('child_process'), 2 | url = require('url'), 3 | _ = require('underscore'), 4 | when = require('when'), 5 | nodefn = require('when/node/function'), 6 | nodemailer = require('nodemailer'); 7 | 8 | function GhostMailer(opts) { 9 | opts = opts || {}; 10 | this.transport = opts.transport || null; 11 | } 12 | 13 | // ## E-mail transport setup 14 | // *This promise should always resolve to avoid halting Ghost::init*. 15 | GhostMailer.prototype.init = function (ghost) { 16 | this.ghost = ghost; 17 | // TODO: fix circular reference ghost -> mail -> api -> ghost, remove this late require 18 | this.api = require('./api'); 19 | 20 | var self = this, 21 | config = ghost.config(); 22 | 23 | if (config.mail && config.mail.transport && config.mail.options) { 24 | this.createTransport(config); 25 | return when.resolve(); 26 | } 27 | 28 | // Attempt to detect and fallback to `sendmail` 29 | return this.detectSendmail().then(function (binpath) { 30 | self.transport = nodemailer.createTransport('sendmail', { 31 | path: binpath 32 | }); 33 | self.usingSendmail(); 34 | }, function () { 35 | self.emailDisabled(); 36 | }).ensure(function () { 37 | return when.resolve(); 38 | }); 39 | }; 40 | 41 | GhostMailer.prototype.isWindows = function () { 42 | return process.platform === 'win32'; 43 | }; 44 | 45 | GhostMailer.prototype.detectSendmail = function () { 46 | if (this.isWindows()) { 47 | return when.reject(); 48 | } 49 | return when.promise(function (resolve, reject) { 50 | cp.exec('which sendmail', function (err, stdout) { 51 | if (err && !/bin\/sendmail/.test(stdout)) { 52 | return reject(); 53 | } 54 | resolve(stdout.toString().replace(/(\n|\r|\r\n)$/, '')); 55 | }); 56 | }); 57 | }; 58 | 59 | GhostMailer.prototype.createTransport = function (config) { 60 | this.transport = nodemailer.createTransport(config.mail.transport, _.clone(config.mail.options)); 61 | }; 62 | 63 | GhostMailer.prototype.usingSendmail = function () { 64 | this.api.notifications.add({ 65 | type: 'info', 66 | message: [ 67 | "Ghost is attempting to use your server's sendmail to send e-mail.", 68 | "It is recommended that you explicitly configure an e-mail service,", 69 | "See http://docs.ghost.org/mail for instructions" 70 | ].join(' '), 71 | status: 'persistent', 72 | id: 'ghost-mail-fallback' 73 | }); 74 | }; 75 | 76 | GhostMailer.prototype.emailDisabled = function () { 77 | this.api.notifications.add({ 78 | type: 'warn', 79 | message: [ 80 | "Ghost is currently unable to send e-mail.", 81 | "See http://docs.ghost.org/mail for instructions" 82 | ].join(' '), 83 | status: 'persistent', 84 | id: 'ghost-mail-disabled' 85 | }); 86 | this.transport = null; 87 | }; 88 | 89 | // Sends an e-mail message enforcing `to` (blog owner) and `from` fields 90 | GhostMailer.prototype.send = function (message) { 91 | if (!this.transport) { 92 | return when.reject(new Error('Email Error: No e-mail transport configured.')); 93 | } 94 | if (!(message && message.subject && message.html)) { 95 | return when.reject(new Error('Email Error: Incomplete message data.')); 96 | } 97 | 98 | var from = this.ghost.config().mail.fromaddress || this.ghost.settings('email'), 99 | to = message.to || this.ghost.settings('email'), 100 | sendMail = nodefn.lift(this.transport.sendMail.bind(this.transport)); 101 | 102 | message = _.extend(message, { 103 | from: from, 104 | to: to, 105 | generateTextFromHTML: true 106 | }); 107 | 108 | return sendMail(message).otherwise(function (error) { 109 | // Proxy the error message so we can add 'Email Error:' to the beginning to make it clearer. 110 | error = _.isString(error) ? 'Email Error:' + error : (_.isObject(error) ? 'Email Error: ' + error.message : 'Email Error: Unknown Email Error'); 111 | return when.reject(new Error(error)); 112 | }); 113 | }; 114 | 115 | module.exports = GhostMailer; 116 | -------------------------------------------------------------------------------- /core/client/assets/sass/layouts/plugins.scss: -------------------------------------------------------------------------------- 1 | /* ============================================================================= 2 | Plugins 3 | ============================================================================= */ 4 | 5 | .settings { 6 | 7 | .plugin-section { 8 | padding-bottom: 20px; 9 | } 10 | 11 | .plugin-section-header { 12 | h3 { 13 | margin: 15px 0; 14 | font-size: 1.1em; 15 | font-weight: normal; 16 | color: $brown; 17 | } 18 | } 19 | 20 | .plugin-section-footer { 21 | text-align: right; 22 | } 23 | 24 | .button-update-all { 25 | @include icon($i-lightning, 1em, #FFC125) { 26 | margin-right: 5px; 27 | }; 28 | } 29 | 30 | .button-cancel { 31 | @include icon($i-x, 1em, #fff) { 32 | margin-right: 5px; 33 | }; 34 | } 35 | 36 | .plugin-section-table { 37 | margin-top: 5px; 38 | 39 | tbody > tr:nth-child(odd) > td { 40 | background: none; 41 | } 42 | 43 | .plugin-section-item { 44 | 45 | &.inactive { 46 | .plugin-meta { 47 | opacity: 0.4; 48 | } 49 | 50 | td:last-child { 51 | .plugin-meta { 52 | opacity: 1; 53 | } 54 | } 55 | } 56 | 57 | td { 58 | padding: 20px 0; 59 | border-bottom:$lightbrown 1px solid; 60 | 61 | &:first-child { 62 | padding-left: 0px; 63 | border-top:$lightbrown 1px solid; 64 | 65 | .plugin-meta { 66 | padding: 0px; 67 | width: 75%; 68 | border-left: none; 69 | text-align: left; 70 | } 71 | } 72 | 73 | &:last-child { 74 | .plugin-meta { 75 | padding: 0px; 76 | text-align: right; 77 | } 78 | } 79 | } 80 | } 81 | 82 | .plugin-icon { 83 | display: inline-block; 84 | width: 40px; 85 | height: 40px; 86 | margin-right: 15px; 87 | background: #FFC125; 88 | border-radius: 5px; 89 | vertical-align: middle; 90 | 91 | img { 92 | width: 100%; 93 | } 94 | } 95 | 96 | .plugin-meta { 97 | @include box-sizing(border-box); 98 | display: inline-block; 99 | width: 100%; 100 | height: 100%; 101 | padding: 0 20px; 102 | vertical-align: middle; 103 | border-left: $lightbrown 1px solid; 104 | text-align: center; 105 | } 106 | 107 | .plugin-info { 108 | display: block; 109 | color: lighten($grey, 5%); 110 | font-size: 1.2em; 111 | font-weight: normal; 112 | vertical-align: top; 113 | } 114 | 115 | .plugin-title { 116 | color: $grey; 117 | } 118 | 119 | .plugin-sub-info { 120 | display: block; 121 | color: $midgrey; 122 | } 123 | 124 | .plugin-download-progress { 125 | position: relative; 126 | display: block; 127 | height: 6px; 128 | margin-top: 10px; 129 | background: $lightbrown; 130 | border-radius: 3px; 131 | 132 | > span { 133 | position: absolute; 134 | left: 0; 135 | top: 0; 136 | content: ""; 137 | height: 100%; 138 | background-color: $blue; 139 | border-radius: 3px; 140 | } 141 | } 142 | 143 | .rating { 144 | 145 | unicode-bidi: bidi-override; 146 | text-align: center; 147 | 148 | > span { 149 | display: inline-block; 150 | position: relative; 151 | width: 1.1em; 152 | height: 1.1em; 153 | font-size: 0.8em; 154 | 155 | &:before { 156 | content: "\2605"; 157 | position: absolute; 158 | left: 0; 159 | opacity: 0.5; 160 | } 161 | 162 | &.active { 163 | &:before { 164 | content: "\2605"; 165 | opacity: 1; 166 | } 167 | } 168 | } 169 | 170 | } 171 | 172 | .plugin-settings-icon { 173 | display: block; 174 | margin-top: 9px; 175 | font-size: 1.4em; 176 | @include icon($i-settings, 1em, $grey); 177 | } 178 | 179 | } //.plugin-section-table 180 | 181 | } //.settings -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Ghost](https://github.com/TryGhost/Ghost) [![Build Status](https://travis-ci.org/TryGhost/Ghost.png?branch=master)](https://travis-ci.org/TryGhost/Ghost) 2 | 3 | Ghost is a free, open, simple blogging platform that's available to anyone who wants to use it. Lovingly created and maintained by [John O'Nolan](http://twitter.com/JohnONolan) + [Hannah Wolfe](http://twitter.com/ErisDS) + an amazing group of [contributors](https://github.com/TryGhost/Ghost/contributors). 4 | 5 | Visit the project's website at [http://ghost.org](http://ghost.org)! 6 | 7 | 8 | ## Getting Started 9 | 10 | There are **two** ways to get started with Ghost: 11 | 12 | 1. **Install from a Release** - these are pre-built zip packages found on [Ghost.org](http://ghost.org/download) which have no dependencies other than node & npm. Installation instructions are below. 13 | 2. **Cloning from the GitHub repo** - requires you to build assets yourself. Instructions can be found in [CONTRIBUTING.md](https://github.com/TryGhost/Ghost/blob/master/CONTRIBUTING.md) 14 | 15 | 16 | ### Installing from a Release 17 | 18 | *Please Note:* Releases are pre-built packages, GitHub releases (tags) are not. To install from GitHub you need to follow the [contributing guide](https://github.com/TryGhost/Ghost/blob/master/CONTRIBUTING.md). 19 | 20 | 1. Once you've downloaded one of the releases, unzip it, and place the directory wherever you would like to run the code 21 | 2. Fire up a terminal (or node command prompt in Windows) and change directory to the root of the Ghost application (where config.example.js and index.js are) 22 | 4. run `npm install --production` to install the node dependencies. If you see `error Error: ENOENT` on this step, make sure you are in the project directory and try again. 23 | 4. To start ghost, run `npm start` 24 | 5. Visit `http://localhost:2368/` in your web browser or go to `http://localhost:2368/ghost` to log in 25 | 26 | Check out the [Documentation](http://docs.ghost.org/) for more detailed instructions, or get in touch via the [forum](http://ghost.org/forum) if you get stuck. 27 | 28 | ### Updating with the latest changes 29 | 30 | Documentation on updating can be found in the [Ghost Guide](http://docs.ghost.org/installation/upgrading/) 31 | 32 | ### Logging in For The First Time 33 | 34 | Once you have the Ghost server up and running, you should be able to navigate to `http://localhost:2368/ghost/` from a web browser, where you will be prompted for a login. 35 | 36 | 1. Click on the "register new user" link 37 | 2. Enter your user details (careful here: There is no password reset yet!) 38 | 3. Return to the login screen and use those details to log in. 39 | 40 | Note - this is still very alpha. Not everything works yet. 41 | 42 | 43 | ## Versioning 44 | 45 | For transparency and insight into our release cycle, and for striving to maintain backward compatibility, Ghost will be maintained according to the [Semantic Versioning](http://semver.org/) guidelines as much as possible. 46 | 47 | Releases will be numbered with the following format: 48 | 49 | `..-` 50 | 51 | Constructed with the following guidelines: 52 | 53 | * A new *major* release indicates a large change where backwards compatibility is broken. 54 | * A new *minor* release indicates a normal change that maintains backwards compatibility. 55 | * A new *patch* release indicates a bugfix or small change which does not affect compatibility. 56 | * A new *build* release indicates this is a pre-release of the version. 57 | 58 | 59 | ## Reporting Bugs and Contributing Code 60 | 61 | Want to report a bug, request a feature, or help us build Ghost? Check out our in depth guide to [Contributing to Ghost](https://github.com/TryGhost/Ghost/blob/master/CONTRIBUTING.md). We need all the help we can get! 62 | 63 | 64 | ## Community 65 | 66 | Keep track of Ghost development and Ghost community activity. 67 | 68 | * Follow Ghost on [Twitter](http://twitter.com/TryGhost), [Facebook](http://facebook.com/tryghostapp) and [Google+](https://plus.google.com/114465948129362706086). 69 | * Read and subscribe to the [The Official Ghost Blog](http://blog.ghost.org). 70 | * Chat with Ghost developers on IRC. We're on `irc.freenode.net`, in the `#Ghost` channel. 71 | 72 | 73 | ## Copyright & License 74 | 75 | Copyright (C) 2013 The Ghost Foundation - Released under the MIT License. 76 | 77 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 78 | 79 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 80 | 81 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 82 | 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. 83 | -------------------------------------------------------------------------------- /core/server/data/import/000.js: -------------------------------------------------------------------------------- 1 | var when = require('when'), 2 | _ = require('underscore'), 3 | models = require('../../models'), 4 | errors = require('../../errorHandling'), 5 | Importer000; 6 | 7 | 8 | Importer000 = function () { 9 | _.bindAll(this, 'basicImport'); 10 | 11 | this.version = '000'; 12 | 13 | this.importFrom = { 14 | '000': this.basicImport, 15 | '001': this.tempImport, 16 | '002': this.tempImport 17 | }; 18 | }; 19 | 20 | Importer000.prototype.importData = function (data) { 21 | return this.canImport(data) 22 | .then(function (importerFunc) { 23 | return importerFunc(data); 24 | }, function (reason) { 25 | return when.reject(reason); 26 | }); 27 | }; 28 | 29 | Importer000.prototype.canImport = function (data) { 30 | if (data.meta && data.meta.version && this.importFrom[data.meta.version]) { 31 | return when.resolve(this.importFrom[data.meta.version]); 32 | } 33 | 34 | return when.reject("Unsupported version of data: " + data.meta.version); 35 | }; 36 | 37 | 38 | function stripProperties(properties, data) { 39 | _.each(data, function (obj) { 40 | _.each(properties, function (property) { 41 | delete obj[property]; 42 | }); 43 | }); 44 | return data; 45 | } 46 | 47 | function preProcessPostTags(tableData) { 48 | var postTags, 49 | postsWithTags = {}; 50 | 51 | 52 | postTags = tableData.posts_tags; 53 | _.each(postTags, function (post_tag) { 54 | if (!postsWithTags.hasOwnProperty(post_tag.post_id)) { 55 | postsWithTags[post_tag.post_id] = []; 56 | } 57 | postsWithTags[post_tag.post_id].push(post_tag.tag_id); 58 | }); 59 | 60 | _.each(postsWithTags, function (tag_ids, post_id) { 61 | var post, tags; 62 | post = _.find(tableData.posts, function (post) { 63 | return post.id === parseInt(post_id, 10); 64 | }); 65 | if (post) { 66 | tags = _.filter(tableData.tags, function (tag) { 67 | return _.indexOf(tag_ids, tag.id) !== -1; 68 | }); 69 | post.tags = []; 70 | _.each(tags, function (tag) { 71 | // names are unique.. this should get the right tags added 72 | // as long as tags are added first; 73 | post.tags.push({name: tag.name}); 74 | }); 75 | } 76 | }); 77 | 78 | return tableData; 79 | } 80 | 81 | function importTags(ops, tableData) { 82 | tableData = stripProperties(['id'], tableData); 83 | _.each(tableData, function (tag) { 84 | ops.push(models.Tag.read({name: tag.name}).then(function (_tag) { 85 | if (!_tag) { 86 | return models.Tag.add(tag); 87 | } 88 | return when.resolve(_tag); 89 | })); 90 | }); 91 | } 92 | 93 | function importPosts(ops, tableData) { 94 | tableData = stripProperties(['id'], tableData); 95 | _.each(tableData, function (post) { 96 | ops.push(models.Post.add(post)); 97 | }); 98 | } 99 | 100 | function importUsers(ops, tableData) { 101 | tableData = stripProperties(['id'], tableData); 102 | tableData[0].id = 1; 103 | ops.push(models.User.edit(tableData[0])); 104 | } 105 | 106 | function importSettings(ops, tableData) { 107 | // for settings we need to update individual settings, and insert any missing ones 108 | // the one setting we MUST NOT update is the databaseVersion settings 109 | var blackList = ['databaseVersion']; 110 | tableData = stripProperties(['id'], tableData); 111 | tableData = _.filter(tableData, function (data) { 112 | return blackList.indexOf(data.key) === -1; 113 | }); 114 | 115 | ops.push(models.Settings.edit(tableData)); 116 | } 117 | 118 | // No data needs modifying, we just import whatever tables are available 119 | Importer000.prototype.basicImport = function (data) { 120 | var ops = [], 121 | tableData = data.data; 122 | 123 | // Do any pre-processing of relationships (we can't depend on ids) 124 | if (tableData.posts_tags && tableData.posts && tableData.tags) { 125 | tableData = preProcessPostTags(tableData); 126 | } 127 | 128 | // Import things in the right order: 129 | if (tableData.tags && tableData.tags.length) { 130 | importTags(ops, tableData.tags); 131 | } 132 | 133 | if (tableData.posts && tableData.posts.length) { 134 | importPosts(ops, tableData.posts); 135 | } 136 | 137 | if (tableData.users && tableData.users.length) { 138 | importUsers(ops, tableData.users); 139 | } 140 | 141 | if (tableData.settings && tableData.settings.length) { 142 | importSettings(ops, tableData.settings); 143 | } 144 | 145 | /** do nothing with these tables, the data shouldn't have changed from the fixtures 146 | * permissions 147 | * roles 148 | * permissions_roles 149 | * permissions_users 150 | * roles_users 151 | */ 152 | 153 | return when.all(ops).then(function (results) { 154 | return when.resolve(results); 155 | }, function (err) { 156 | return when.reject("Error importing data: " + err.message || err, err.stack); 157 | }); 158 | }; 159 | 160 | module.exports = { 161 | Importer000: Importer000, 162 | importData: function (data) { 163 | return new Importer000().importData(data); 164 | } 165 | }; -------------------------------------------------------------------------------- /core/server/models/settings.js: -------------------------------------------------------------------------------- 1 | var Settings, 2 | GhostBookshelf = require('./base'), 3 | validator = GhostBookshelf.validator, 4 | uuid = require('node-uuid'), 5 | _ = require('underscore'), 6 | errors = require('../errorHandling'), 7 | when = require('when'), 8 | defaultSettings; 9 | 10 | // For neatness, the defaults file is split into categories. 11 | // It's much easier for us to work with it as a single level 12 | // instead of iterating those categories every time 13 | function parseDefaultSettings() { 14 | var defaultSettingsInCategories = require('../data/default-settings.json'), 15 | defaultSettingsFlattened = {}; 16 | 17 | 18 | _.each(defaultSettingsInCategories, function (settings, categoryName) { 19 | _.each(settings, function (setting, settingName) { 20 | setting.type = categoryName; 21 | setting.key = settingName; 22 | defaultSettingsFlattened[settingName] = setting; 23 | }); 24 | }); 25 | 26 | return defaultSettingsFlattened; 27 | } 28 | defaultSettings = parseDefaultSettings(); 29 | 30 | // Each setting is saved as a separate row in the database, 31 | // but the overlying API treats them as a single key:value mapping 32 | Settings = GhostBookshelf.Model.extend({ 33 | 34 | tableName: 'settings', 35 | 36 | permittedAttributes: ['id', 'uuid', 'key', 'value', 'type', 'created_at', 'created_by', 'updated_at', 'update_by'], 37 | 38 | defaults: function () { 39 | return { 40 | uuid: uuid.v4(), 41 | type: 'core' 42 | }; 43 | }, 44 | 45 | 46 | // Validate default settings using the validator module. 47 | // Each validation's key is a name and its value is an array of options 48 | // Use true (boolean) if options aren't applicable 49 | // 50 | // eg: 51 | // validations: { isUrl: true, len: [20, 40] } 52 | // 53 | // will validate that a setting's length is a URL between 20 and 40 chars, 54 | // available validators: https://github.com/chriso/node-validator#list-of-validation-methods 55 | validate: function () { 56 | validator.check(this.get('key'), "Setting key cannot be blank").notEmpty(); 57 | validator.check(this.get('type'), "Setting type cannot be blank").notEmpty(); 58 | 59 | var matchingDefault = defaultSettings[this.get('key')]; 60 | 61 | if (matchingDefault && matchingDefault.validations) { 62 | _.each(matchingDefault.validations, function (validationOptions, validationName) { 63 | var validation = validator.check(this.get('value')); 64 | 65 | if (validationOptions === true) { 66 | validationOptions = null; 67 | } 68 | if (typeof validationOptions !== 'array') { 69 | validationOptions = [validationOptions]; 70 | } 71 | 72 | // equivalent of validation.isSomething(option1, option2) 73 | validation[validationName].apply(validation, validationOptions); 74 | }, this); 75 | } 76 | }, 77 | 78 | 79 | saving: function () { 80 | 81 | // All blog setting keys that need their values to be escaped. 82 | if (this.get('type') === 'blog' && _.contains(['title', 'description', 'email'], this.get('key'))) { 83 | this.set('value', this.sanitize('value')); 84 | } 85 | 86 | return GhostBookshelf.Model.prototype.saving.apply(this, arguments); 87 | } 88 | 89 | }, { 90 | read: function (_key) { 91 | // Allow for just passing the key instead of attributes 92 | if (!_.isObject(_key)) { 93 | _key = { key: _key }; 94 | } 95 | return GhostBookshelf.Model.read.call(this, _key); 96 | }, 97 | 98 | edit: function (_data) { 99 | var settings = this; 100 | if (!Array.isArray(_data)) { 101 | _data = [_data]; 102 | } 103 | return when.map(_data, function (item) { 104 | // Accept an array of models as input 105 | if (item.toJSON) { item = item.toJSON(); } 106 | return settings.forge({ key: item.key }).fetch().then(function (setting) { 107 | if (setting) { 108 | return setting.set('value', item.value).save(); 109 | } 110 | return settings.forge({ key: item.key, value: item.value }).save(); 111 | 112 | }, errors.logAndThrowError); 113 | }); 114 | }, 115 | 116 | populateDefaults: function () { 117 | return this.findAll().then(function (allSettings) { 118 | var usedKeys = allSettings.models.map(function (setting) { return setting.get('key'); }), 119 | insertOperations = []; 120 | 121 | _.each(defaultSettings, function (defaultSetting, defaultSettingKey) { 122 | var isMissingFromDB = usedKeys.indexOf(defaultSettingKey) === -1; 123 | // Temporary code to deal with old databases with currentVersion settings 124 | if (defaultSettingKey === 'databaseVersion' && usedKeys.indexOf('currentVersion') !== -1) { 125 | isMissingFromDB = false; 126 | } 127 | if (isMissingFromDB) { 128 | defaultSetting.value = defaultSetting.defaultValue; 129 | insertOperations.push(Settings.forge(defaultSetting).save()); 130 | } 131 | }); 132 | 133 | return when.all(insertOperations); 134 | }); 135 | } 136 | 137 | }); 138 | 139 | module.exports = { 140 | Settings: Settings 141 | }; 142 | -------------------------------------------------------------------------------- /core/test/unit/model_users_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, before, beforeEach, afterEach, it*/ 2 | var testUtils = require('./testUtils'), 3 | should = require('should'), 4 | when = require('when'), 5 | _ = require('underscore'), 6 | errors = require('../../server/errorHandling'), 7 | 8 | // Stuff we are testing 9 | Models = require('../../server/models'); 10 | 11 | 12 | describe('User Model', function run() { 13 | var UserModel = Models.User; 14 | 15 | before(function (done) { 16 | testUtils.clearData().then(function () { 17 | done(); 18 | }, done); 19 | }); 20 | 21 | afterEach(function (done) { 22 | testUtils.clearData().then(function () { 23 | done(); 24 | }, done); 25 | }); 26 | 27 | describe('Registration', function runRegistration() { 28 | beforeEach(function (done) { 29 | this.timeout(5000); 30 | testUtils.initData().then(function () { 31 | done(); 32 | }, done); 33 | }); 34 | 35 | it('can add first', function (done) { 36 | var userData = testUtils.DataGenerator.forModel.users[0]; 37 | 38 | UserModel.add(userData).then(function (createdUser) { 39 | should.exist(createdUser); 40 | createdUser.has('uuid').should.equal(true); 41 | createdUser.attributes.password.should.not.equal(userData.password, "password was hashed"); 42 | createdUser.attributes.email.should.eql(userData.email, "email address correct"); 43 | 44 | done(); 45 | }).then(null, done); 46 | }); 47 | }); 48 | 49 | describe('Basic Operations', function () { 50 | 51 | beforeEach(function (done) { 52 | this.timeout(5000); 53 | testUtils.initData() 54 | .then(function () { 55 | return when(testUtils.insertDefaultUser()); 56 | }) 57 | .then(function () { 58 | done(); 59 | }, done); 60 | }); 61 | 62 | it('can\'t add second', function (done) { 63 | var userData = testUtils.DataGenerator.forModel.users[1]; 64 | 65 | return UserModel.add(userData).then(done, function (failure) { 66 | failure.message.should.eql('A user is already registered. Only one user for now!'); 67 | done(); 68 | }).then(null, done); 69 | }); 70 | 71 | it('can browse', function (done) { 72 | 73 | UserModel.browse().then(function (results) { 74 | should.exist(results); 75 | 76 | results.length.should.be.above(0); 77 | 78 | done(); 79 | 80 | }).then(null, done); 81 | }); 82 | 83 | it('can read', function (done) { 84 | var firstUser; 85 | 86 | UserModel.browse().then(function (results) { 87 | 88 | should.exist(results); 89 | 90 | results.length.should.be.above(0); 91 | 92 | firstUser = results.models[0]; 93 | 94 | return UserModel.read({email: firstUser.attributes.email}); 95 | 96 | }).then(function (found) { 97 | 98 | should.exist(found); 99 | 100 | found.attributes.name.should.equal(firstUser.attributes.name); 101 | 102 | done(); 103 | 104 | }).then(null, done); 105 | 106 | }); 107 | 108 | it('can edit', function (done) { 109 | var firstUser; 110 | 111 | UserModel.browse().then(function (results) { 112 | 113 | should.exist(results); 114 | 115 | results.length.should.be.above(0); 116 | 117 | firstUser = results.models[0]; 118 | 119 | return UserModel.edit({id: firstUser.id, website: "some.newurl.com"}); 120 | 121 | }).then(function (edited) { 122 | 123 | should.exist(edited); 124 | 125 | edited.attributes.website.should.equal('some.newurl.com'); 126 | 127 | done(); 128 | 129 | }).then(null, done); 130 | }); 131 | 132 | it("can get effective permissions", function (done) { 133 | UserModel.effectivePermissions(1).then(function (effectivePermissions) { 134 | should.exist(effectivePermissions); 135 | 136 | effectivePermissions.length.should.be.above(0); 137 | 138 | done(); 139 | }).then(null, done); 140 | }); 141 | 142 | it('can delete', function (done) { 143 | var firstUserId; 144 | 145 | UserModel.browse().then(function (results) { 146 | 147 | should.exist(results); 148 | 149 | results.length.should.be.above(0); 150 | 151 | firstUserId = results.models[0].id; 152 | 153 | return UserModel.destroy(firstUserId); 154 | 155 | }).then(function () { 156 | 157 | return UserModel.browse(); 158 | 159 | }).then(function (newResults) { 160 | var ids, hasDeletedId; 161 | 162 | if (newResults.length < 1) { 163 | // Bug out if we only had one user and deleted it. 164 | return done(); 165 | } 166 | 167 | ids = _.pluck(newResults.models, "id"); 168 | hasDeletedId = _.any(ids, function (id) { 169 | return id === firstUserId; 170 | }); 171 | 172 | hasDeletedId.should.equal(false); 173 | done(); 174 | 175 | }).then(null, done); 176 | }); 177 | }); 178 | 179 | }); -------------------------------------------------------------------------------- /core/client/views/login.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, $, _, Backbone, JST */ 2 | (function () { 3 | "use strict"; 4 | 5 | Ghost.Views.Login = Ghost.View.extend({ 6 | 7 | initialize: function () { 8 | this.render(); 9 | $(".js-login-box").css({"opacity": 0}).animate({"opacity": 1}, 500, function () { 10 | $("[name='email']").focus(); 11 | }); 12 | }, 13 | 14 | templateName: "login", 15 | 16 | events: { 17 | 'submit #login': 'submitHandler' 18 | }, 19 | 20 | submitHandler: function (event) { 21 | event.preventDefault(); 22 | var email = this.$el.find('.email').val(), 23 | password = this.$el.find('.password').val(), 24 | redirect = Ghost.Views.Utils.getUrlVariables().r; 25 | 26 | Ghost.Validate._errors = []; 27 | Ghost.Validate.check(email).isEmail(); 28 | Ghost.Validate.check(password, "Please enter a password").len(0); 29 | 30 | if (Ghost.Validate._errors.length > 0) { 31 | Ghost.Validate.handleErrors(); 32 | } else { 33 | $.ajax({ 34 | url: '/ghost/signin/', 35 | type: 'POST', 36 | data: { 37 | email: email, 38 | password: password, 39 | redirect: redirect 40 | }, 41 | success: function (msg) { 42 | window.location.href = msg.redirect; 43 | }, 44 | error: function (xhr) { 45 | Ghost.notifications.addItem({ 46 | type: 'error', 47 | message: Ghost.Views.Utils.getRequestErrorMessage(xhr), 48 | status: 'passive' 49 | }); 50 | } 51 | }); 52 | } 53 | } 54 | }); 55 | 56 | Ghost.Views.Signup = Ghost.View.extend({ 57 | 58 | initialize: function () { 59 | this.render(); 60 | $(".js-signup-box").css({"opacity": 0}).animate({"opacity": 1}, 500, function () { 61 | $("[name='name']").focus(); 62 | }); 63 | }, 64 | 65 | templateName: "signup", 66 | 67 | events: { 68 | 'submit #signup': 'submitHandler' 69 | }, 70 | 71 | submitHandler: function (event) { 72 | event.preventDefault(); 73 | var name = this.$el.find('.name').val(), 74 | email = this.$el.find('.email').val(), 75 | password = this.$el.find('.password').val(); 76 | 77 | // This is needed due to how error handling is done. If this is not here, there will not be a time 78 | // when there is no error. 79 | Ghost.Validate._errors = []; 80 | Ghost.Validate.check(name, "Please enter a name").len(1); 81 | Ghost.Validate.check(email, "Please enter a correct email address").isEmail(); 82 | Ghost.Validate.check(password, "Your password is not long enough. It must be at least 8 characters long.").len(8); 83 | 84 | if (Ghost.Validate._errors.length > 0) { 85 | Ghost.Validate.handleErrors(); 86 | } else { 87 | $.ajax({ 88 | url: '/ghost/signup/', 89 | type: 'POST', 90 | data: { 91 | name: name, 92 | email: email, 93 | password: password 94 | }, 95 | success: function (msg) { 96 | window.location.href = msg.redirect; 97 | }, 98 | error: function (xhr) { 99 | Ghost.notifications.addItem({ 100 | type: 'error', 101 | message: Ghost.Views.Utils.getRequestErrorMessage(xhr), 102 | status: 'passive' 103 | }); 104 | } 105 | }); 106 | } 107 | } 108 | }); 109 | 110 | Ghost.Views.Forgotten = Ghost.View.extend({ 111 | 112 | initialize: function () { 113 | this.render(); 114 | $(".js-forgotten-box").css({"opacity": 0}).animate({"opacity": 1}, 500, function () { 115 | $("[name='email']").focus(); 116 | }); 117 | }, 118 | 119 | templateName: "forgotten", 120 | 121 | events: { 122 | 'submit #forgotten': 'submitHandler' 123 | }, 124 | 125 | submitHandler: function (event) { 126 | event.preventDefault(); 127 | 128 | var email = this.$el.find('.email').val(); 129 | 130 | Ghost.Validate._errors = []; 131 | Ghost.Validate.check(email).isEmail(); 132 | 133 | if (Ghost.Validate._errors.length > 0) { 134 | Ghost.Validate.handleErrors(); 135 | } else { 136 | $.ajax({ 137 | url: '/ghost/forgotten/', 138 | type: 'POST', 139 | data: { 140 | email: email 141 | }, 142 | success: function (msg) { 143 | 144 | window.location.href = msg.redirect; 145 | }, 146 | error: function (xhr) { 147 | Ghost.notifications.addItem({ 148 | type: 'error', 149 | message: Ghost.Views.Utils.getRequestErrorMessage(xhr), 150 | status: 'passive' 151 | }); 152 | } 153 | }); 154 | } 155 | } 156 | }); 157 | }()); 158 | -------------------------------------------------------------------------------- /core/test/unit/mail_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, beforeEach, afterEach, it*/ 2 | var testUtils = require('./testUtils'), 3 | should = require('should'), 4 | sinon = require('sinon'), 5 | when = require('when'), 6 | 7 | _ = require("underscore"), 8 | cp = require('child_process'), 9 | 10 | // Stuff we are testing 11 | Ghost = require('../../ghost'), 12 | defaultConfig = require('../../../config'), 13 | SMTP, 14 | SENDMAIL, 15 | fakeConfig, 16 | fakeSettings, 17 | fakeSendmail, 18 | sandbox = sinon.sandbox.create(), 19 | ghost; 20 | 21 | // Mock SMTP config 22 | SMTP = { 23 | transport: 'SMTP', 24 | options: { 25 | service: 'Gmail', 26 | auth: { 27 | user: 'nil', 28 | pass: '123' 29 | } 30 | } 31 | }; 32 | 33 | // Mock Sendmail config 34 | SENDMAIL = { 35 | transport: 'sendmail', 36 | options: { 37 | path: '/nowhere/sendmail' 38 | } 39 | }; 40 | 41 | describe("Mail", function () { 42 | 43 | beforeEach(function () { 44 | // Mock config and settings 45 | fakeConfig = _.extend({}, defaultConfig); 46 | fakeSettings = { 47 | url: 'http://test.tryghost.org', 48 | email: 'ghost-test@localhost' 49 | }; 50 | fakeSendmail = '/fake/bin/sendmail'; 51 | 52 | ghost = new Ghost(); 53 | 54 | sandbox.stub(ghost, "config", function () { 55 | return fakeConfig; 56 | }); 57 | 58 | sandbox.stub(ghost, "settings", function () { 59 | return fakeSettings; 60 | }); 61 | 62 | sandbox.stub(ghost.mail, "isWindows", function () { 63 | return false; 64 | }); 65 | 66 | sandbox.stub(ghost.mail, "detectSendmail", function () { 67 | return when.resolve(fakeSendmail); 68 | }); 69 | }); 70 | 71 | afterEach(function () { 72 | sandbox.restore(); 73 | }); 74 | 75 | it('should attach mail provider to ghost instance', function () { 76 | should.exist(ghost.mail); 77 | ghost.mail.should.have.property('init'); 78 | ghost.mail.should.have.property('transport'); 79 | ghost.mail.should.have.property('send').and.be.a('function'); 80 | }); 81 | 82 | it('should setup SMTP transport on initialization', function (done) { 83 | fakeConfig.mail = SMTP; 84 | ghost.mail.init(ghost).then(function(){ 85 | ghost.mail.should.have.property('transport'); 86 | ghost.mail.transport.transportType.should.eql('SMTP'); 87 | ghost.mail.transport.sendMail.should.be.a('function'); 88 | done(); 89 | }).then(null, done); 90 | }); 91 | 92 | it('should setup sendmail transport on initialization', function (done) { 93 | fakeConfig.mail = SENDMAIL; 94 | ghost.mail.init(ghost).then(function(){ 95 | ghost.mail.should.have.property('transport'); 96 | ghost.mail.transport.transportType.should.eql('SENDMAIL'); 97 | ghost.mail.transport.sendMail.should.be.a('function'); 98 | done(); 99 | }).then(null, done); 100 | }); 101 | 102 | it('should fallback to sendmail if no config set', function (done) { 103 | fakeConfig.mail = null; 104 | ghost.mail.init(ghost).then(function(){ 105 | ghost.mail.should.have.property('transport'); 106 | ghost.mail.transport.transportType.should.eql('SENDMAIL'); 107 | ghost.mail.transport.options.path.should.eql(fakeSendmail); 108 | done(); 109 | }).then(null, done); 110 | }); 111 | 112 | it('should fallback to sendmail if config is empty', function (done) { 113 | fakeConfig.mail = {}; 114 | ghost.mail.init(ghost).then(function(){ 115 | ghost.mail.should.have.property('transport'); 116 | ghost.mail.transport.transportType.should.eql('SENDMAIL'); 117 | ghost.mail.transport.options.path.should.eql(fakeSendmail); 118 | done(); 119 | }).then(null, done); 120 | }); 121 | 122 | it('should disable transport if config is empty & sendmail not found', function (done) { 123 | fakeConfig.mail = {}; 124 | ghost.mail.detectSendmail.restore(); 125 | sandbox.stub(ghost.mail, "detectSendmail", when.reject); 126 | ghost.mail.init(ghost).then(function(){ 127 | should.not.exist(ghost.mail.transport); 128 | done(); 129 | }).then(null, done); 130 | }); 131 | 132 | it('should disable transport if config is empty & platform is win32', function (done) { 133 | fakeConfig.mail = {}; 134 | ghost.mail.detectSendmail.restore(); 135 | ghost.mail.isWindows.restore(); 136 | sandbox.stub(ghost.mail, 'isWindows', function(){ return true }); 137 | ghost.mail.init(ghost).then(function(){ 138 | should.not.exist(ghost.mail.transport); 139 | done(); 140 | }).then(null, done); 141 | }); 142 | 143 | it('should fail to send messages when no transport is set', function (done) { 144 | ghost.mail.detectSendmail.restore(); 145 | sandbox.stub(ghost.mail, "detectSendmail", when.reject); 146 | ghost.mail.init(ghost).then(function(){ 147 | ghost.mail.send().then(function(){ 148 | should.fail(); 149 | done(); 150 | }, function (err) { 151 | err.should.be.an.instanceOf(Error); 152 | done(); 153 | }); 154 | }); 155 | }); 156 | 157 | it('should fail to send messages when given insufficient data', function (done) { 158 | when.settle([ 159 | ghost.mail.send(), 160 | ghost.mail.send({}), 161 | ghost.mail.send({ subject: '123' }), 162 | ghost.mail.send({ subject: '', html: '123' }) 163 | ]).then(function (descriptors) { 164 | descriptors.forEach(function (d) { 165 | d.state.should.equal('rejected'); 166 | d.reason.should.be.an.instanceOf(Error); 167 | }); 168 | done(); 169 | }); 170 | }); 171 | 172 | }); 173 | -------------------------------------------------------------------------------- /core/shared/vendor/showdown/extensions/github.js: -------------------------------------------------------------------------------- 1 | // 2 | // Github Extension (WIP) 3 | // ~~strike-through~~ -> strike-through 4 | // 5 | 6 | (function () { 7 | var github = function (converter) { 8 | return [ 9 | { 10 | // strike-through 11 | // NOTE: showdown already replaced "~" with "~T", so we need to adjust accordingly. 12 | type : 'lang', 13 | regex : '(~T){2}([^~]+)(~T){2}', 14 | replace : function (match, prefix, content, suffix) { 15 | return '' + content + ''; 16 | } 17 | }, 18 | { 19 | // GFM newline and underscore modifications, happen BEFORE showdown 20 | type : 'lang', 21 | filter : function (text) { 22 | var preExtractions = {}, 23 | imageMarkdownRegex = /^(?:\{(.*?)\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim, 24 | hashID = 0; 25 | 26 | function hashId() { 27 | return hashID++; 28 | } 29 | 30 | // Extract pre blocks 31 | text = text.replace(/
    [\s\S]*?<\/pre>/gim, function (x) {
     32 |                         var hash = hashId();
     33 |                         preExtractions[hash] = x;
     34 |                         return "{gfm-js-extract-pre-" + hash + "}";
     35 |                     }, 'm');
     36 | 
     37 |                     //prevent foo_bar and foo_bar_baz from ending up with an italic word in the middle
     38 |                     text = text.replace(/(^(?! {4}|\t)\w+_\w+_\w[\w_]*)/gm, function (x) {
     39 |                         return x.replace(/_/gm, '\\_');
     40 |                     });
     41 | 
     42 |                     // in very clear cases, let newlines become 
    tags 43 | text = text.replace(/^[\w\<][^\n]*\n+/gm, function (x) { 44 | return x.match(/\n{2}/) ? x : x.trim() + " \n"; 45 | }); 46 | 47 | // better URL support, but no title support 48 | text = text.replace(imageMarkdownRegex, function (match, key, alt, src) { 49 | if (src) { 50 | return '' + alt + ''; 51 | } 52 | 53 | return ''; 54 | }); 55 | 56 | text = text.replace(/\{gfm-js-extract-pre-([0-9]+)\}/gm, function (x, y) { 57 | return "\n\n" + preExtractions[y]; 58 | }); 59 | 60 | 61 | return text; 62 | } 63 | }, 64 | { 65 | // GFM autolinking & custom image handling, happens AFTER showdown 66 | type : 'html', 67 | filter : function (text) { 68 | var refExtractions = {}, 69 | preExtractions = {}, 70 | hashID = 0; 71 | 72 | function hashId() { 73 | return hashID++; 74 | } 75 | 76 | // Extract pre blocks 77 | text = text.replace(/<(pre|code)>[\s\S]*?<\/(\1)>/gim, function (x) { 78 | var hash = hashId(); 79 | preExtractions[hash] = x; 80 | return "{gfm-js-extract-pre-" + hash + "}"; 81 | }, 'm'); 82 | 83 | // filter out def urls 84 | // from Marked https://github.com/chjj/marked/blob/master/lib/marked.js#L24 85 | text = text.replace(/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/gmi, 86 | function (x) { 87 | var hash = hashId(); 88 | refExtractions[hash] = x; 89 | return "{gfm-js-extract-ref-url-" + hash + "}"; 90 | }); 91 | 92 | // match a URL 93 | // adapted from https://gist.github.com/jorilallo/1283095#L158 94 | // and http://blog.stevenlevithan.com/archives/mimic-lookbehind-javascript 95 | text = text.replace(/(\]\(|\]|\[|]*?\>)?https?\:\/\/[^"\s\<\>]*[^.,;'">\:\s\<\>\)\]\!]/gmi, 96 | function (wholeMatch, lookBehind, matchIndex) { 97 | // Check we are not inside an HTML tag 98 | var left = text.slice(0, matchIndex), right = text.slice(matchIndex); 99 | if ((left.match(/<[^>]+$/) && right.match(/^[^>]*>/)) || lookBehind) { 100 | return wholeMatch; 101 | } 102 | // If we have a matching lookBehind, this is a failure, else wrap the match in tag 103 | return lookBehind ? wholeMatch : "" + wholeMatch + ""; 104 | }); 105 | 106 | // match email 107 | text = text.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/gmi, function (wholeMatch) { 108 | return "" + wholeMatch + ""; 109 | }); 110 | 111 | // replace extractions 112 | text = text.replace(/\{gfm-js-extract-pre-([0-9]+)\}/gm, function (x, y) { 113 | return preExtractions[y]; 114 | }); 115 | 116 | text = text.replace(/\{gfm-js-extract-ref-url-([0-9]+)\}/gi, function (x, y) { 117 | return "\n\n" + refExtractions[y]; 118 | }); 119 | 120 | return text; 121 | } 122 | } 123 | ]; 124 | }; 125 | 126 | // Client-side export 127 | if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) { window.Showdown.extensions.github = github; } 128 | // Server-side export 129 | if (typeof module !== 'undefined') module.exports = github; 130 | }()); -------------------------------------------------------------------------------- /core/server/controllers/frontend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main controller for Ghost frontend 3 | */ 4 | 5 | /*global require, module */ 6 | 7 | var Ghost = require('../../ghost'), 8 | api = require('../api'), 9 | RSS = require('rss'), 10 | _ = require('underscore'), 11 | errors = require('../errorHandling'), 12 | when = require('when'), 13 | url = require('url'), 14 | 15 | 16 | ghost = new Ghost(), 17 | frontendControllers; 18 | 19 | frontendControllers = { 20 | 'homepage': function (req, res, next) { 21 | // Parse the page number 22 | var pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1, 23 | postsPerPage = parseInt(ghost.settings('postsPerPage'), 10), 24 | options = {}; 25 | 26 | // No negative pages 27 | if (isNaN(pageParam) || pageParam < 1) { 28 | //redirect to 404 page? 29 | return res.redirect('/'); 30 | } 31 | options.page = pageParam; 32 | 33 | // Redirect '/page/1/' to '/' for all teh good SEO 34 | if (pageParam === 1 && req.route.path === '/page/:page/') { 35 | return res.redirect('/'); 36 | } 37 | 38 | // No negative posts per page, must be number 39 | if (!isNaN(postsPerPage) && postsPerPage > 0) { 40 | options.limit = postsPerPage; 41 | } 42 | 43 | api.posts.browse(options).then(function (page) { 44 | 45 | var maxPage = page.pages; 46 | 47 | // A bit of a hack for situations with no content. 48 | if (maxPage === 0) { 49 | maxPage = 1; 50 | page.pages = 1; 51 | } 52 | 53 | // If page is greater than number of pages we have, redirect to last page 54 | if (pageParam > maxPage) { 55 | return res.redirect(maxPage === 1 ? '/' : ('/page/' + maxPage + '/')); 56 | } 57 | 58 | // Render the page of posts 59 | ghost.doFilter('prePostsRender', page.posts, function (posts) { 60 | res.render('index', {posts: posts, pagination: {page: page.page, prev: page.prev, next: page.next, limit: page.limit, total: page.total, pages: page.pages}}); 61 | }); 62 | }).otherwise(function (err) { 63 | return next(new Error(err)); 64 | }); 65 | }, 66 | 'single': function (req, res, next) { 67 | api.posts.read({'slug': req.params.slug}).then(function (post) { 68 | if (post) { 69 | ghost.doFilter('prePostsRender', post.toJSON(), function (post) { 70 | res.render('post', {post: post}); 71 | }); 72 | } else { 73 | next(); 74 | } 75 | 76 | }).otherwise(function (err) { 77 | return next(new Error(err)); 78 | }); 79 | }, 80 | 'rss': function (req, res, next) { 81 | // Initialize RSS 82 | var siteUrl = ghost.config().url, 83 | pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1, 84 | feed; 85 | //needs refact for multi user to not use first user as default 86 | api.users.read({id : 1}).then(function (user) { 87 | feed = new RSS({ 88 | title: ghost.settings('title'), 89 | description: ghost.settings('description'), 90 | generator: 'Ghost v' + res.locals.version, 91 | author: user ? user.attributes.name : null, 92 | feed_url: url.resolve(siteUrl, '/rss/'), 93 | site_url: siteUrl, 94 | ttl: '60' 95 | }); 96 | 97 | // No negative pages 98 | if (isNaN(pageParam) || pageParam < 1) { 99 | return res.redirect('/rss/'); 100 | } 101 | 102 | if (pageParam === 1 && req.route.path === '/rss/:page/') { 103 | return res.redirect('/rss/'); 104 | } 105 | 106 | api.posts.browse({page: pageParam}).then(function (page) { 107 | var maxPage = page.pages; 108 | 109 | // A bit of a hack for situations with no content. 110 | if (maxPage === 0) { 111 | maxPage = 1; 112 | page.pages = 1; 113 | } 114 | 115 | // If page is greater than number of pages we have, redirect to last page 116 | if (pageParam > maxPage) { 117 | return res.redirect('/rss/' + maxPage + '/'); 118 | } 119 | 120 | ghost.doFilter('prePostsRender', page.posts, function (posts) { 121 | posts.forEach(function (post) { 122 | var item = { 123 | title: _.escape(post.title), 124 | guid: post.uuid, 125 | url: siteUrl + '/' + post.slug + '/', 126 | date: post.published_at, 127 | }, 128 | content = post.html; 129 | 130 | //set img src to absolute url 131 | content = content.replace(/src=["|'|\s]?([\w\/\?\$\.\+\-;%:@&=,_]+)["|'|\s]?/gi, function (match, p1) { 132 | p1 = url.resolve(siteUrl, p1); 133 | return "src='" + p1 + "' "; 134 | }); 135 | //set a href to absolute url 136 | content = content.replace(/href=["|'|\s]?([\w\/\?\$\.\+\-;%:@&=,_]+)["|'|\s]?/gi, function (match, p1) { 137 | p1 = url.resolve(siteUrl, p1); 138 | return "href='" + p1 + "' "; 139 | }); 140 | item.description = content; 141 | feed.item(item); 142 | }); 143 | res.set('Content-Type', 'text/xml'); 144 | res.send(feed.xml()); 145 | }); 146 | }); 147 | }).otherwise(function (err) { 148 | return next(new Error(err)); 149 | }); 150 | } 151 | 152 | }; 153 | 154 | module.exports = frontendControllers; --------------------------------------------------------------------------------