├── core ├── client │ ├── tpl │ │ ├── modals │ │ │ ├── blank.hbs │ │ │ ├── copyToHTML.hbs │ │ │ ├── uploadImage.hbs │ │ │ └── markdown.hbs │ │ ├── notification.hbs │ │ ├── settings │ │ │ ├── sidebar.hbs │ │ │ ├── general.hbs │ │ │ └── user-profile.hbs │ │ ├── forgotten.hbs │ │ ├── reset.hbs │ │ ├── login.hbs │ │ ├── signup.hbs │ │ ├── list-item.hbs │ │ ├── modal.hbs │ │ └── preview.hbs │ ├── assets │ │ ├── sass │ │ │ ├── ie.scss │ │ │ ├── modules │ │ │ │ ├── breakpoint │ │ │ │ │ ├── _no-query.scss │ │ │ │ │ ├── parsers │ │ │ │ │ │ ├── single │ │ │ │ │ │ │ └── _default.scss │ │ │ │ │ │ ├── double │ │ │ │ │ │ │ ├── _double-string.scss │ │ │ │ │ │ │ ├── _default.scss │ │ │ │ │ │ │ └── _default-pair.scss │ │ │ │ │ │ ├── triple │ │ │ │ │ │ │ └── _default.scss │ │ │ │ │ │ ├── _single.scss │ │ │ │ │ │ ├── _resolution.scss │ │ │ │ │ │ ├── _triple.scss │ │ │ │ │ │ ├── _double.scss │ │ │ │ │ │ ├── resolution │ │ │ │ │ │ │ └── _resolution.scss │ │ │ │ │ │ └── _query.scss │ │ │ │ │ ├── _respond-to.scss │ │ │ │ │ ├── _parsers.scss │ │ │ │ │ └── _context.scss │ │ │ │ ├── animations.scss │ │ │ │ └── breakpoint.scss │ │ │ ├── screen.scss │ │ │ └── layouts │ │ │ │ └── errors.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 │ │ │ ├── codemirror │ │ │ ├── addon │ │ │ │ └── mode │ │ │ │ │ └── overlay.js │ │ │ └── mode │ │ │ │ └── gfm │ │ │ │ ├── index.html │ │ │ │ ├── gfm.js │ │ │ │ └── test.js │ │ │ ├── showdown │ │ │ └── extensions │ │ │ │ └── ghostdown.js │ │ │ └── icheck │ │ │ └── jquery.icheck.min.js │ ├── models │ │ ├── tag.js │ │ ├── themes.js │ │ ├── settings.js │ │ ├── user.js │ │ ├── base.js │ │ ├── widget.js │ │ ├── uploadModal.js │ │ └── post.js │ ├── helpers │ │ └── index.js │ ├── toggle.js │ ├── mobile-interactions.js │ ├── router.js │ └── init.js ├── test │ ├── utils │ │ ├── fixtures │ │ │ ├── test.hbs │ │ │ └── theme │ │ │ │ └── partials │ │ │ │ └── test.hbs │ │ ├── api.js │ │ └── index.js │ ├── blanket_coverage.js │ ├── functional │ │ ├── frontend │ │ │ ├── route_test.js │ │ │ ├── error_test.js │ │ │ ├── post_test.js │ │ │ ├── feed_test.js │ │ │ └── home_test.js │ │ ├── admin │ │ │ ├── flow_test.js │ │ │ └── logout_test.js │ │ └── api │ │ │ └── tags_test.js │ ├── unit │ │ ├── server_helpers_template_spec.js │ │ ├── server_spec.js │ │ ├── export_spec.js │ │ ├── plugin_proxy_spec.js │ │ └── client_ghostdown_spec.js │ └── integration │ │ └── model │ │ ├── model_roles_spec.js │ │ └── model_permissions_spec.js ├── server │ ├── views │ │ ├── reset.hbs │ │ ├── login.hbs │ │ ├── forgotten.hbs │ │ ├── signup.hbs │ │ ├── settings.hbs │ │ ├── partials │ │ │ ├── notifications.hbs │ │ │ └── navbar.hbs │ │ ├── content.hbs │ │ ├── error.hbs │ │ ├── debug.hbs │ │ ├── user-error.hbs │ │ └── default.hbs │ ├── data │ │ ├── import │ │ │ ├── 001.js │ │ │ └── index.js │ │ ├── export │ │ │ └── index.js │ │ └── default-settings.json │ ├── routes │ │ ├── index.js │ │ ├── frontend.js │ │ ├── api.js │ │ └── admin.js │ ├── helpers │ │ ├── tpl │ │ │ ├── nav.hbs │ │ │ └── pagination.hbs │ │ └── template.js │ ├── permissions │ │ └── objectTypeModelMap.js │ ├── api │ │ ├── tags.js │ │ ├── notifications.js │ │ ├── index.js │ │ └── users.js │ ├── storage │ │ ├── index.js │ │ ├── base.js │ │ └── localfilesystem.js │ ├── plugins │ │ ├── proxy.js │ │ ├── loader.js │ │ └── index.js │ ├── api.js │ ├── middleware.js │ ├── models │ │ ├── session.js │ │ ├── role.js │ │ ├── permission.js │ │ ├── tag.js │ │ └── index.js │ ├── config │ │ ├── theme.js │ │ └── index.js │ ├── require-tree.js │ ├── middleware │ │ └── ghost-busboy.js │ ├── filters.js │ └── bookshelf-session.js ├── shared │ ├── img │ │ ├── user-cover.png │ │ └── user-image.png │ ├── lang │ │ ├── en_US.json │ │ └── i18n.js │ └── favicon.ico ├── index.js └── server.js ├── Gemfile ├── content ├── plugins │ └── README.md ├── images │ └── README.md └── data │ └── README.md ├── .gitmodules ├── index.js ├── Gemfile.lock ├── SECURITY.md ├── .travis.yml ├── .gitignore ├── LICENSE └── package.json /core/client/tpl/modals/blank.hbs: -------------------------------------------------------------------------------- 1 | {{{content.text}}} -------------------------------------------------------------------------------- /core/test/utils/fixtures/test.hbs: -------------------------------------------------------------------------------- 1 |

HelloWorld

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

HelloWorld Themed

-------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sass' 4 | gem 'bourbon' 5 | -------------------------------------------------------------------------------- /content/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Content / Plugins 2 | 3 | Coming soon, Ghost plugins will appear here. -------------------------------------------------------------------------------- /core/server/views/reset.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
3 | 4 |
-------------------------------------------------------------------------------- /core/server/views/login.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /core/shared/img/user-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/Ghost/master/core/shared/img/user-cover.png -------------------------------------------------------------------------------- /core/shared/img/user-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/Ghost/master/core/shared/img/user-image.png -------------------------------------------------------------------------------- /core/client/assets/img/large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/Ghost/master/core/client/assets/img/large.png -------------------------------------------------------------------------------- /core/client/assets/img/medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/Ghost/master/core/client/assets/img/medium.png -------------------------------------------------------------------------------- /core/client/assets/img/small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/Ghost/master/core/client/assets/img/small.png -------------------------------------------------------------------------------- /core/server/views/forgotten.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
3 | 4 |
-------------------------------------------------------------------------------- /core/server/views/signup.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /core/client/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/Ghost/master/core/client/assets/fonts/icons.eot -------------------------------------------------------------------------------- /core/client/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/Ghost/master/core/client/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /core/client/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/Ghost/master/core/client/assets/fonts/icons.woff -------------------------------------------------------------------------------- /core/client/assets/img/404-ghost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/Ghost/master/core/client/assets/img/404-ghost.png -------------------------------------------------------------------------------- /core/client/assets/img/loadingcat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/Ghost/master/core/client/assets/img/loadingcat.gif -------------------------------------------------------------------------------- /.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/404-ghost@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/Ghost/master/core/client/assets/img/404-ghost@2x.png -------------------------------------------------------------------------------- /core/client/assets/img/touch-icon-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/Ghost/master/core/client/assets/img/touch-icon-ipad.png -------------------------------------------------------------------------------- /core/client/assets/img/touch-icon-iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/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 | 
-------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // # Ghost bootloader 2 | // Orchestrates the loading of Ghost 3 | // When run from command line. 4 | 5 | var ghost = require('./core'); 6 | 7 | ghost(); -------------------------------------------------------------------------------- /core/server/data/import/001.js: -------------------------------------------------------------------------------- 1 | var Importer000 = require('./000'); 2 | 3 | module.exports = { 4 | Importer001: Importer000, 5 | importData: function (data) { 6 | return new Importer000.importData(data); 7 | } 8 | }; -------------------------------------------------------------------------------- /core/client/tpl/notification.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{{message}}} 3 | 4 |
5 | -------------------------------------------------------------------------------- /core/server/routes/index.js: -------------------------------------------------------------------------------- 1 | var api = require('./api'), 2 | admin = require('./admin'), 3 | frontend = require('./frontend'); 4 | 5 | module.exports = { 6 | api: api, 7 | admin: admin, 8 | frontend: frontend 9 | }; -------------------------------------------------------------------------------- /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.ProgressCollection.extend({ 6 | url: Ghost.paths.apiRoot + '/tags/' 7 | }); 8 | }()); 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | bourbon (3.1.8) 5 | sass (>= 3.2.0) 6 | thor 7 | sass (3.2.12) 8 | thor (0.18.1) 9 | 10 | PLATFORMS 11 | ruby 12 | 13 | DEPENDENCIES 14 | bourbon 15 | sass 16 | -------------------------------------------------------------------------------- /core/client/models/themes.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, $, _, Backbone */ 2 | (function () { 3 | 'use strict'; 4 | 5 | Ghost.Models.Themes = Backbone.Model.extend({ 6 | url: Ghost.paths.apiRoot + '/themes' 7 | }); 8 | 9 | }()); 10 | -------------------------------------------------------------------------------- /core/client/tpl/settings/sidebar.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Settings

3 |
4 | -------------------------------------------------------------------------------- /core/test/blanket_coverage.js: -------------------------------------------------------------------------------- 1 | var blanket = require("blanket")({ 2 | "pattern": ["/core/server/", "/core/client/", "/core/shared/"], 3 | "data-cover-only": ["/core/server/", "/core/client/", "/core/shared/"] 4 | }), 5 | requireDir = require("require-dir"); 6 | 7 | requireDir("./unit"); 8 | requireDir("./integration"); -------------------------------------------------------------------------------- /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 repository. 6 | 7 | Thanks for helping make Ghost safe for everyone. 8 | -------------------------------------------------------------------------------- /core/client/tpl/forgotten.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /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.ProgressModel.extend({ 6 | url: Ghost.paths.apiRoot + '/settings/?type=blog,theme', 7 | id: '0' 8 | }); 9 | 10 | }()); 11 | -------------------------------------------------------------------------------- /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/client/tpl/modals/uploadImage.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /core/server/helpers/tpl/pagination.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/server/api/tags.js: -------------------------------------------------------------------------------- 1 | var dataProvider = require('../models'), 2 | tags; 3 | 4 | 5 | tags = { 6 | // #### All 7 | 8 | // **takes:** Nothing yet 9 | all: function browse() { 10 | // **returns:** a promise for all tags which have previously been used in a json object 11 | return dataProvider.Tag.findAll(); 12 | } 13 | }; 14 | 15 | module.exports = tags; -------------------------------------------------------------------------------- /core/client/models/user.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, $, _, Backbone */ 2 | (function () { 3 | 'use strict'; 4 | 5 | Ghost.Models.User = Ghost.ProgressModel.extend({ 6 | url: Ghost.paths.apiRoot + '/users/me/' 7 | }); 8 | 9 | // Ghost.Collections.Users = Backbone.Collection.extend({ 10 | // url: Ghost.paths.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 archive page routing 3 | */ 4 | 5 | /*globals CasperTest, 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/client/assets/sass/modules/breakpoint/_no-query.scss: -------------------------------------------------------------------------------- 1 | @function breakpoint-no-query($query) { 2 | @if type-of($query) == 'list' { 3 | $keyword: nth($query, 1); 4 | 5 | @if type-of($keyword) == 'string' and ($keyword == 'no-query' or $keyword == 'no query' or $keyword == 'fallback') { 6 | @return nth($query, 2); 7 | } 8 | @else { 9 | @return false; 10 | } 11 | } 12 | @else { 13 | @return false; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/server/views/partials/notifications.hbs: -------------------------------------------------------------------------------- 1 | {{#if messages}} 2 | {{#each messages}} 3 |
4 |
5 | {{{message}}} 6 | 7 |
8 |
9 | {{/each}} 10 | {{/if}} 11 | -------------------------------------------------------------------------------- /core/client/tpl/reset.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 | 9 |
10 | -------------------------------------------------------------------------------- /core/client/assets/sass/modules/breakpoint/parsers/single/_default.scss: -------------------------------------------------------------------------------- 1 | @function breakpoint-parse-default($feature) { 2 | $default: $breakpoint-default-feature; 3 | 4 | // Set Context 5 | $context-setter: private-breakpoint-set-context($default, $feature); 6 | 7 | @if ($breakpoint-to-ems == true) and (type-of($feature) == 'number') { 8 | @return '#{$default}: #{breakpoint-to-base-em($feature)}'; 9 | } 10 | @else { 11 | @return '#{$default}: #{$feature}'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/index.js: -------------------------------------------------------------------------------- 1 | // # Ghost bootloader 2 | // Orchestrates the loading of Ghost 3 | // When run from command line. 4 | 5 | var config = require('./server/config'), 6 | errors = require('./server/errorHandling'); 7 | 8 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 9 | 10 | function startGhost(app) { 11 | config.load().then(function () { 12 | var ghost = require('./server'); 13 | ghost(app); 14 | }).otherwise(errors.logAndThrowError); 15 | } 16 | 17 | module.exports = startGhost; -------------------------------------------------------------------------------- /core/client/assets/sass/modules/breakpoint/parsers/double/_double-string.scss: -------------------------------------------------------------------------------- 1 | @function breakpoint-parse-double-string($first, $second) { 2 | $feature: ''; 3 | $value: ''; 4 | 5 | // Test to see which is the feature and which is the value 6 | @if (breakpoint-string-value($first) == true) { 7 | $feature: $first; 8 | $value: $second; 9 | } 10 | @else { 11 | $feature: $second; 12 | $value: $first; 13 | } 14 | 15 | // Set Context 16 | $context-setter: private-breakpoint-set-context($feature, $value); 17 | 18 | @return '(#{$feature}: #{$value})'; 19 | } 20 | -------------------------------------------------------------------------------- /core/client/assets/sass/modules/breakpoint/parsers/double/_default.scss: -------------------------------------------------------------------------------- 1 | @function breakpoint-parse-double-default($first, $second) { 2 | $feature: ''; 3 | $value: ''; 4 | 5 | @if type-of($first) == 'string' { 6 | $feature: $first; 7 | $value: $second; 8 | } 9 | @else { 10 | $feature: $second; 11 | $value: $first; 12 | } 13 | 14 | // Set Context 15 | $context-setter: private-breakpoint-set-context($feature, $value); 16 | 17 | @if ($breakpoint-to-ems == true) { 18 | $value: breakpoint-to-base-em($value); 19 | } 20 | 21 | @return '(#{$feature}: #{$value})' 22 | } 23 | -------------------------------------------------------------------------------- /core/client/tpl/login.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 | 9 |
10 | Forgotten password? 11 |
12 |
13 | -------------------------------------------------------------------------------- /core/client/tpl/signup.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 |
9 | 10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /core/server/storage/index.js: -------------------------------------------------------------------------------- 1 | var errors = require('../errorHandling'), 2 | storage; 3 | 4 | function get_storage() { 5 | // TODO: this is where the check for storage plugins should go 6 | // Local file system is the default 7 | var storageChoice = 'localfilesystem'; 8 | 9 | if (storage) { 10 | return storage; 11 | } 12 | 13 | try { 14 | // TODO: determine if storage has all the necessary methods 15 | storage = require('./' + storageChoice); 16 | } catch (e) { 17 | errors.logError(e); 18 | } 19 | return storage; 20 | } 21 | 22 | module.exports.get_storage = get_storage; -------------------------------------------------------------------------------- /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 |
    18 | -------------------------------------------------------------------------------- /core/client/assets/sass/modules/breakpoint/parsers/triple/_default.scss: -------------------------------------------------------------------------------- 1 | @function breakpoint-parse-triple-default($feature, $first, $second) { 2 | 3 | // Sort into min and max 4 | $min: min($first, $second); 5 | $max: max($first, $second); 6 | 7 | // Set Context 8 | $context-setter: private-breakpoint-set-context(min-#{$feature}, $min); 9 | $context-setter: private-breakpoint-set-context(max-#{$feature}, $max); 10 | 11 | // Make them EMs if need be 12 | @if ($breakpoint-to-ems == true) { 13 | $min: breakpoint-to-base-em($min); 14 | $max: breakpoint-to-base-em($max); 15 | } 16 | 17 | @return '(min-#{$feature}: #{$min}) and (max-#{$feature}: #{$max})'; 18 | } 19 | -------------------------------------------------------------------------------- /core/server/plugins/proxy.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | api = require('../api'), 3 | helpers = require('../helpers'), 4 | filters = require('../filters'); 5 | 6 | var proxy = { 7 | 8 | filters: { 9 | register: filters.registerFilter, 10 | unregister: filters.unregisterFilter 11 | }, 12 | helpers: { 13 | register: helpers.registerThemeHelper, 14 | registerAsync: helpers.registerAsyncThemeHelper 15 | }, 16 | api: { 17 | posts: _.pick(api.posts, 'browse', 'read'), 18 | tags: api.tags, 19 | notifications: _.pick(api.notifications, 'add'), 20 | settings: _.pick(api.settings, 'read') 21 | } 22 | }; 23 | 24 | module.exports = proxy; -------------------------------------------------------------------------------- /core/client/assets/sass/modules/breakpoint/parsers/double/_default-pair.scss: -------------------------------------------------------------------------------- 1 | @function breakpoint-parse-default-pair($first, $second) { 2 | $default: $breakpoint-default-pair; 3 | $min: ''; 4 | $max: ''; 5 | 6 | // Sort into min and max 7 | $min: min($first, $second); 8 | $max: max($first, $second); 9 | 10 | // Set Context 11 | $context-setter: private-breakpoint-set-context(min-#{$default}, $min); 12 | $context-setter: private-breakpoint-set-context(max-#{$default}, $max); 13 | 14 | // Make them EMs if need be 15 | @if ($breakpoint-to-ems == true) { 16 | $min: breakpoint-to-base-em($min); 17 | $max: breakpoint-to-base-em($max); 18 | } 19 | 20 | @return '(min-#{$default}: #{$min}) and (max-#{$default}: #{$max})'; 21 | } 22 | -------------------------------------------------------------------------------- /core/test/functional/frontend/error_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests if RSS exists and is working 3 | */ 4 | /*globals CasperTest, casper */ 5 | CasperTest.begin('Check post not found (404)', 2, function suite(test) { 6 | casper.thenOpen(url + 'asdf/', function (response) { 7 | test.assertEqual(response.status, 404, 'Response status should be 404.'); 8 | test.assertSelectorHasText('.error-code', '404'); 9 | }); 10 | }, true); 11 | 12 | CasperTest.begin('Check frontend route not found (404)', 2, function suite(test) { 13 | casper.thenOpen(url + 'asdf/asdf/', function (response) { 14 | test.assertEqual(response.status, 404, 'Response status should be 404.'); 15 | test.assertSelectorHasText('.error-code', '404'); 16 | }); 17 | }, true); -------------------------------------------------------------------------------- /core/client/tpl/list-item.hbs: -------------------------------------------------------------------------------- 1 | 2 |

    {{{title}}}

    3 |
    4 | 5 | {{#if published}} 6 | {{#if page}} 7 | Page 8 | {{else}} 9 | 12 | {{/if}} 13 | {{else}} 14 | Draft 15 | {{/if}} 16 | 17 |
    18 |
    19 | -------------------------------------------------------------------------------- /core/server/api.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file is a shim to accomodate simple file system merge upgrade from 0.3.3 to 0.4. 3 | During a file system merge upgrade from 0.3.3 to 0.4, the old version of this file will 4 | persist unless this shim is in place. Problems arise when the stale 0.3.3 version of 5 | this file exists as well as the new directory of files bearing the same name in 0.4. 6 | Node's require defaults to loading the 0.3.3 version causing errors. This file replaces 7 | the old 0.3.3 version. This allows all dependent modules to continue requiring their 8 | dependencies the way they always have. See issue 1873 for more information. 9 | 10 | https://github.com/TryGhost/Ghost/issues/1873 11 | */ 12 | var api = require('./api/index.js'); 13 | 14 | module.exports = api; 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | env: 5 | - DB=sqlite3 6 | - DB=mysql 7 | - DB=pg 8 | matrix: 9 | allow_failures: 10 | - env: DB=pg 11 | before_install: 12 | - git submodule update --init 13 | - gem update --system 14 | - gem install sass bourbon 15 | - npm install -g grunt-cli 16 | - git clone git://github.com/n1k0/casperjs.git ~/casperjs 17 | - cd ~/casperjs 18 | - git checkout tags/1.1-beta3 19 | - export PATH=$PATH:`pwd`/bin 20 | - cd - 21 | - if [ $DB == "mysql" ]; then mysql -e 'create database ghost_travis'; fi 22 | - if [ $DB == "pg" ]; then npm install pg; psql -c 'create database ghost_travis;' -U postgres; fi 23 | before_script: 24 | - phantomjs --version 25 | - casperjs --version 26 | - grunt init 27 | -------------------------------------------------------------------------------- /core/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file is a shim to accomodate simple file system merge upgrade from 0.3.3 to 0.4. 3 | During a file system merge upgrade from 0.3.3 to 0.4, the old version of this file will 4 | persist unless this shim is in place. Problems arise when the stale 0.3.3 version of 5 | this file exists as well as the new directory of files bearing the same name in 0.4. 6 | Node's require defaults to loading the 0.3.3 version causing errors. This file replaces 7 | the old 0.3.3 version. This allows all dependent modules to continue requiring their 8 | dependencies the way they always have. See issue 1873 for more information. 9 | 10 | https://github.com/TryGhost/Ghost/issues/1873 11 | */ 12 | var server = require('./server/index.js'); 13 | 14 | module.exports = server; 15 | -------------------------------------------------------------------------------- /core/server/middleware.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file is a shim to accomodate simple file system merge upgrade from 0.3.3 to 0.4. 3 | During a file system merge upgrade from 0.3.3 to 0.4, the old version of this file will 4 | persist unless this shim is in place. Problems arise when the stale 0.3.3 version of 5 | this file exists as well as the new directory of files bearing the same name in 0.4. 6 | Node's require defaults to loading the 0.3.3 version causing errors. This file replaces 7 | the old 0.3.3 version. This allows all dependent modules to continue requiring their 8 | dependencies the way they always have. See issue 1873 for more information. 9 | 10 | https://github.com/TryGhost/Ghost/issues/1873 11 | */ 12 | var middleware = require('./middleware/index.js'); 13 | 14 | module.exports = middleware; 15 | -------------------------------------------------------------------------------- /core/client/assets/sass/modules/breakpoint/parsers/_single.scss: -------------------------------------------------------------------------------- 1 | ////////////////////////////// 2 | // Import Pieces 3 | ////////////////////////////// 4 | @import "single/default"; 5 | 6 | @function breakpoint-parse-single($feature, $empty-media, $first) { 7 | $parsed: ''; 8 | $leader: ''; 9 | // If we're forcing 10 | @if not ($empty-media) or not ($first) { 11 | $leader: 'and '; 12 | } 13 | 14 | // If it's a single feature that can stand alone, we let it 15 | @if (breakpoint-single-string($feature)) { 16 | $parsed: $feature; 17 | // Set Context 18 | $context-setter: private-breakpoint-set-context($feature, $feature); 19 | } 20 | // If it's not a stand alone feature, we pass it off to the default handler. 21 | @else { 22 | $parsed: breakpoint-parse-default($feature); 23 | } 24 | 25 | @return $leader + '(' + $parsed + ')'; 26 | } 27 | -------------------------------------------------------------------------------- /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/server/helpers/template.js: -------------------------------------------------------------------------------- 1 | var templates = {}, 2 | hbs = require('express-hbs'), 3 | errors = require('../errorHandling'); 4 | 5 | // ## Template utils 6 | 7 | // Execute a template helper 8 | // All template helpers are register as partial view. 9 | templates.execute = function (name, context) { 10 | 11 | var partial = hbs.handlebars.partials[name]; 12 | 13 | if (partial === undefined) { 14 | errors.logAndThrowError('Template ' + name + ' not found.'); 15 | return; 16 | } 17 | 18 | // If the partial view is not compiled, it compiles and saves in handlebars 19 | if (typeof partial === 'string') { 20 | partial = hbs.handlebars.compile(partial); 21 | hbs.handlebars.partials[name] = partial; 22 | } 23 | 24 | return new hbs.handlebars.SafeString(partial(context)); 25 | }; 26 | 27 | module.exports = templates; -------------------------------------------------------------------------------- /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/models/session.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | Session, 3 | Sessions; 4 | 5 | Session = ghostBookshelf.Model.extend({ 6 | 7 | tableName: 'sessions', 8 | 9 | permittedAttributes: ['id', 'expires', 'sess'], 10 | 11 | saving: function () { 12 | // Remove any properties which don't belong on the session model 13 | this.attributes = this.pick(this.permittedAttributes); 14 | } 15 | }, { 16 | destroyAll: function (options) { 17 | options = options || {}; 18 | return ghostBookshelf.Collection.forge([], {model: this}).fetch(). 19 | then(function (collection) { 20 | collection.invokeThen('destroy', options); 21 | }); 22 | } 23 | }); 24 | 25 | Sessions = ghostBookshelf.Collection.extend({ 26 | model: Session 27 | }); 28 | 29 | module.exports = { 30 | Session: Session, 31 | Sessions: Sessions 32 | }; -------------------------------------------------------------------------------- /core/test/unit/server_helpers_template_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, beforeEach, it*/ 2 | var testUtils = require('../utils'), 3 | should = require('should'), 4 | sinon = require('sinon'), 5 | when = require('when'), 6 | _ = require('underscore'), 7 | path = require('path'), 8 | hbs = require('express-hbs'), 9 | 10 | // Stuff we are testing 11 | config = require('../../server/config'), 12 | api = require('../../server/api'), 13 | template = require('../../server/helpers/template'); 14 | 15 | describe('Helpers Template', function () { 16 | 17 | it("can execute a template", function () { 18 | hbs.registerPartial('test', '

    Hello {{name}}

    '); 19 | 20 | var safeString = template.execute('test', {name: 'world'}); 21 | 22 | should.exist(safeString); 23 | safeString.should.have.property('string').and.equal('

    Hello world

    '); 24 | }); 25 | }); -------------------------------------------------------------------------------- /core/test/unit/server_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, beforeEach, it*/ 2 | var net = require('net'), 3 | assert = require('assert'), 4 | should = require('should'), 5 | request = require('request'), 6 | server = require('../../server'), 7 | config = require('../../../config'); 8 | 9 | describe('Server', function () { 10 | var port = config.testing.server.port, 11 | host = config.testing.server.host, 12 | url = 'http://' + host + ':' + port; 13 | 14 | 15 | it('should not start a connect server when required', function (done) { 16 | request(url, function (error, response, body) { 17 | assert.equal(response, undefined); 18 | assert.equal(body, undefined); 19 | assert.notEqual(error, undefined); 20 | assert.equal(error.code, 'ECONNREFUSED'); 21 | done(); 22 | }); 23 | }); 24 | 25 | }); 26 | 27 | -------------------------------------------------------------------------------- /core/client/assets/sass/modules/breakpoint/parsers/_resolution.scss: -------------------------------------------------------------------------------- 1 | @import "resolution/resolution"; 2 | 3 | @function breakpoint-build-resolution($query-print, $query-resolution, $empty-media, $first) { 4 | $leader: ''; 5 | // If we're forcing 6 | @if not ($empty-media) or not ($first) { 7 | $leader: 'and '; 8 | } 9 | 10 | @if $breakpoint-resolutions and $query-resolution { 11 | $resolutions: breakpoint-make-resolutions($query-resolution); 12 | $length: length($resolutions); 13 | $query-holder: ''; 14 | 15 | @for $i from 1 through $length { 16 | $query: '#{$query-print} #{$leader}#{nth($resolutions, $i)}'; 17 | @if $i == 1 { 18 | $query-holder: $query; 19 | } 20 | @else { 21 | $query-holder: '#{$query-holder}, #{$query}'; 22 | } 23 | } 24 | 25 | @return $query-holder; 26 | } 27 | @else { 28 | // Return with attached resolution 29 | @return $query-print; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core/client/assets/sass/modules/breakpoint/parsers/_triple.scss: -------------------------------------------------------------------------------- 1 | ////////////////////////////// 2 | // Import Pieces 3 | ////////////////////////////// 4 | @import "triple/default"; 5 | 6 | @function breakpoint-parse-triple($feature, $empty-media, $first) { 7 | $parsed: ''; 8 | $leader: ''; 9 | 10 | // If we're forcing 11 | @if not ($empty-media) or not ($first) { 12 | $leader: 'and '; 13 | } 14 | 15 | // separate the string features from the value numbers 16 | $string: null; 17 | $numbers: null; 18 | @each $val in $feature { 19 | @if type-of($val) == string { 20 | $string: $val; 21 | } 22 | @else { 23 | @if type-of($numbers) == 'null' { 24 | $numbers: $val; 25 | } 26 | @else { 27 | $numbers: append($numbers, $val); 28 | } 29 | } 30 | } 31 | 32 | $parsed: breakpoint-parse-triple-default($string, nth($numbers, 1), nth($numbers, 2)); 33 | 34 | @return $leader + $parsed; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /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/routes/frontend.js: -------------------------------------------------------------------------------- 1 | var frontend = require('../controllers/frontend'); 2 | 3 | module.exports = function (server) { 4 | /*jslint regexp: true */ 5 | 6 | // ### Frontend routes 7 | server.get('/rss/', frontend.rss); 8 | server.get('/rss/:page/', frontend.rss); 9 | server.get('/page/:page/', frontend.homepage); 10 | // Only capture the :slug part of the URL 11 | // This regex will always have two capturing groups, 12 | // one for date, and one for the slug. 13 | // Examples: 14 | // Given `/plain-slug/` the req.params would be [undefined, 'plain-slug'] 15 | // Given `/2012/12/24/plain-slug/` the req.params would be ['2012/12/24/', 'plain-slug'] 16 | // Given `/plain-slug/edit/` the req.params would be [undefined, 'plain-slug', 'edit'] 17 | server.get(/^\/([0-9]{4}\/[0-9]{2}\/[0-9]{2}\/)?([^\/.]*)\/$/, frontend.single); 18 | server.get(/^\/([0-9]{4}\/[0-9]{2}\/[0-9]{2}\/)?([^\/.]*)\/edit\/$/, frontend.edit); 19 | server.get('/', frontend.homepage); 20 | }; -------------------------------------------------------------------------------- /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 |
    -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /.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/tmp/* 45 | /content/data/* 46 | /content/plugins/**/* 47 | /content/themes/**/* 48 | /content/images/**/* 49 | !/content/themes/casper/** 50 | !/README.md 51 | 52 | # Changelog, which is autogenerated, not committed 53 | CHANGELOG.md 54 | 55 | # Casper generated files 56 | /core/test/functional/*.png 57 | 58 | config.js 59 | 60 | # Built asset files 61 | /core/built 62 | 63 | # Coverage reports 64 | coverage.html 65 | -------------------------------------------------------------------------------- /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 | 26 |
    27 | {{/if}} 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 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/client/assets/sass/modules/breakpoint/parsers/_double.scss: -------------------------------------------------------------------------------- 1 | ////////////////////////////// 2 | // Import Pieces 3 | ////////////////////////////// 4 | @import "double/default-pair"; 5 | @import "double/double-string"; 6 | @import "double/default"; 7 | 8 | @function breakpoint-parse-double($feature, $empty-media, $first) { 9 | $parsed: ''; 10 | $leader: ''; 11 | // If we're forcing 12 | @if not ($empty-media) or not ($first) { 13 | $leader: 'and '; 14 | } 15 | 16 | $first: nth($feature, 1); 17 | $second: nth($feature, 2); 18 | 19 | // If we've got two numbers, we know we need to use the default pair because there are no media queries that has a media feature that is a number 20 | @if type-of($first) == 'number' and type-of($second) == 'number' { 21 | $parsed: breakpoint-parse-default-pair($first, $second); 22 | } 23 | // If they are both strings, we send it through the string parser 24 | @else if type-of($first) == 'string' and type-of($second) == 'string' { 25 | $parsed: breakpoint-parse-double-string($first, $second); 26 | } 27 | // If it's a string/number pair, we parse it as a normal double 28 | @else { 29 | $parsed: breakpoint-parse-double-default($first, $second); 30 | } 31 | 32 | @return $leader + $parsed; 33 | } 34 | -------------------------------------------------------------------------------- /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 ghostBookshelf.Model.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/server/config/theme.js: -------------------------------------------------------------------------------- 1 | // Holds all theme configuration information 2 | // that as mostly used by templates and handlebar helpers. 3 | 4 | var when = require('when'), 5 | 6 | // Variables 7 | themeConfig = {}; 8 | 9 | 10 | function theme() { 11 | return themeConfig; 12 | } 13 | 14 | // We must pass the api.settings object 15 | // into this method due to circular dependencies. 16 | // If we were to require the api module here 17 | // there would be a race condition where the ./models/base 18 | // tries to access the config() object before it is created. 19 | function update(settings, configUrl) { 20 | return when.all([ 21 | settings.read('title'), 22 | settings.read('description'), 23 | settings.read('logo'), 24 | settings.read('cover') 25 | ]).then(function (globals) { 26 | // normalise the URL by removing any trailing slash 27 | themeConfig.url = configUrl.replace(/\/$/, ''); 28 | themeConfig.title = globals[0].value; 29 | themeConfig.description = globals[1].value; 30 | themeConfig.logo = globals[2] ? globals[2].value : ''; 31 | themeConfig.cover = globals[3] ? globals[3].value : ''; 32 | return; 33 | }); 34 | } 35 | 36 | module.exports = theme; 37 | module.exports.update = update; 38 | -------------------------------------------------------------------------------- /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.ProgressModel = 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.Model.prototype.fetch.call(this, options); 21 | } 22 | }); 23 | 24 | Ghost.ProgressCollection = Backbone.Collection.extend({ 25 | 26 | // Adds in a call to start a loading bar 27 | // This is sets up a success function which completes the loading bar 28 | fetch : function (options) { 29 | options = options || {}; 30 | 31 | NProgress.start(); 32 | 33 | options.success = function () { 34 | NProgress.done(); 35 | }; 36 | 37 | return Backbone.Collection.prototype.fetch.call(this, options); 38 | } 39 | }); 40 | }()); -------------------------------------------------------------------------------- /core/client/models/widget.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, $, _, Backbone */ 2 | (function () { 3 | 'use strict'; 4 | 5 | Ghost.Models.Widget = Ghost.ProgressModel.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 = Ghost.ProgressCollection.extend({ 39 | // url: Ghost.paths.apiRoot + '/widgets/', // What will this be? 40 | model: Ghost.Models.Widget 41 | }); 42 | 43 | }()); 44 | -------------------------------------------------------------------------------- /core/server/models/index.js: -------------------------------------------------------------------------------- 1 | var migrations = require('../data/migration'), 2 | _ = require('underscore'); 3 | 4 | module.exports = { 5 | Post: require('./post').Post, 6 | User: require('./user').User, 7 | Role: require('./role').Role, 8 | Permission: require('./permission').Permission, 9 | Settings: require('./settings').Settings, 10 | Tag: require('./tag').Tag, 11 | Base: require('./base'), 12 | Session: require('./session').Session, 13 | 14 | init: function () { 15 | return migrations.init(); 16 | }, 17 | reset: function () { 18 | return migrations.reset().then(function () { 19 | return migrations.init(); 20 | }); 21 | }, 22 | // ### deleteAllContent 23 | // Delete all content from the database (posts, tags, tags_posts) 24 | deleteAllContent: function () { 25 | var self = this; 26 | 27 | return self.Post.browse().then(function (posts) { 28 | _.each(posts.toJSON(), function (post) { 29 | self.Post.destroy(post.id); 30 | }); 31 | }).then(function () { 32 | self.Tag.browse().then(function (tags) { 33 | _.each(tags.toJSON(), function (tag) { 34 | self.Tag.destroy(tag.id); 35 | }); 36 | }); 37 | }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /core/client/models/uploadModal.js: -------------------------------------------------------------------------------- 1 | /*global Ghost, Backbone, $ */ 2 | (function () { 3 | 'use strict'; 4 | Ghost.Models.uploadModal = Backbone.Model.extend({ 5 | 6 | options: { 7 | close: true, 8 | type: 'action', 9 | style: ["wide"], 10 | animation: 'fade', 11 | afterRender: function () { 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 | this.options.acceptEncoding = options.acceptEncoding || 'image/*'; 35 | } 36 | }); 37 | 38 | }()); 39 | -------------------------------------------------------------------------------- /core/server/config/index.js: -------------------------------------------------------------------------------- 1 | // General entry point for all configuration data 2 | // 3 | // This file itself is a wrapper for the root level config.js file. 4 | // All other files that need to reference config.js should use this file. 5 | 6 | var loader = require('./loader'), 7 | paths = require('./paths'), 8 | theme = require('./theme'), 9 | ghostConfig; 10 | 11 | // Returns NODE_ENV config object 12 | function config() { 13 | // @TODO: get rid of require statement. 14 | // This is currently needed for tests to load config file 15 | // successfully. While running application we should never 16 | // have to directly delegate to the config.js file. 17 | return ghostConfig || require(paths().config)[process.env.NODE_ENV]; 18 | } 19 | 20 | function loadConfig() { 21 | return loader().then(function (config) { 22 | // Cache the config.js object's environment 23 | // object so we can later refer to it. 24 | // Note: this is not the entirety of config.js, 25 | // just the object appropriate for this NODE_ENV 26 | ghostConfig = config; 27 | 28 | // can't load theme settings yet as we don't have the API, 29 | // but we can load the paths 30 | return paths.update(config.url); 31 | }); 32 | } 33 | 34 | config.load = loadConfig; 35 | config.paths = paths; 36 | config.theme = theme; 37 | 38 | module.exports = config; -------------------------------------------------------------------------------- /core/server/data/export/index.js: -------------------------------------------------------------------------------- 1 | var when = require('when'), 2 | _ = require('underscore'), 3 | migration = require('../migration'), 4 | knex = require('../../models/base').knex, 5 | schema = require('../schema').tables, 6 | 7 | excludedTables = ['sessions'], 8 | exporter; 9 | 10 | exporter = function () { 11 | var tablesToExport = _.keys(schema); 12 | 13 | return when.join(migration.getDatabaseVersion(), tablesToExport).then(function (results) { 14 | var version = results[0], 15 | tables = results[1], 16 | selectOps = _.map(tables, function (name) { 17 | if (excludedTables.indexOf(name) < 0) { 18 | return knex(name).select(); 19 | } 20 | }); 21 | 22 | return when.all(selectOps).then(function (tableData) { 23 | var exportData = { 24 | meta: { 25 | exported_on: new Date().getTime(), 26 | version: version 27 | }, 28 | data: { 29 | // Filled below 30 | } 31 | }; 32 | 33 | _.each(tables, function (name, i) { 34 | exportData.data[name] = tableData[i]; 35 | }); 36 | 37 | return when.resolve(exportData); 38 | }, function (err) { 39 | console.log("Error exporting data: " + err); 40 | }); 41 | }); 42 | }; 43 | 44 | module.exports = exporter; -------------------------------------------------------------------------------- /core/server/views/partials/navbar.hbs: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /core/server/api/notifications.js: -------------------------------------------------------------------------------- 1 | var when = require('when'), 2 | _ = require('underscore'), 3 | // Holds the persistent notifications 4 | notificationsStore = [], 5 | notifications; 6 | 7 | // ## Notifications 8 | notifications = { 9 | 10 | browse: function browse() { 11 | return when(notificationsStore); 12 | }, 13 | 14 | // #### Destroy 15 | 16 | // **takes:** an identifier object ({id: id}) 17 | destroy: function destroy(i) { 18 | notificationsStore = _.reject(notificationsStore, function (element) { 19 | return element.id === i.id; 20 | }); 21 | // **returns:** a promise for remaining notifications as a json object 22 | return when(notificationsStore); 23 | }, 24 | 25 | destroyAll: function destroyAll() { 26 | notificationsStore = []; 27 | return when(notificationsStore); 28 | }, 29 | 30 | // #### Add 31 | 32 | // **takes:** a notification object of the form 33 | // ``` 34 | // msg = { 35 | // type: 'error', // this can be 'error', 'success', 'warn' and 'info' 36 | // message: 'This is an error', // A string. Should fit in one line. 37 | // status: 'persistent', // or 'passive' 38 | // id: 'auniqueid' // A unique ID 39 | // }; 40 | // ``` 41 | add: function add(notification) { 42 | // **returns:** a promise for all notifications as a json object 43 | return when(notificationsStore.push(notification)); 44 | } 45 | }; 46 | 47 | module.exports = notifications; -------------------------------------------------------------------------------- /core/shared/lang/i18n.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | config = require('../../server/config'), 3 | /** 4 | * Create new Polyglot object 5 | * @type {Polyglot} 6 | */ 7 | I18n; 8 | 9 | I18n = function (ghost) { 10 | 11 | // TODO: validate 12 | var lang = ghost.settings('defaultLang'), 13 | path = config.paths().lang, 14 | langFilePath = path + lang + '.json'; 15 | 16 | return function (req, res, next) { 17 | 18 | if (lang === 'en_US') { 19 | // TODO: do stuff here to optimise for en 20 | 21 | // Make jslint empty block error go away 22 | lang = 'en_US'; 23 | } 24 | 25 | /** TODO: potentially use req.acceptedLanguages rather than the default 26 | * TODO: handle loading language file for frontend on frontend request etc 27 | * TODO: switch this mess to be promise driven */ 28 | fs.stat(langFilePath, function (error) { 29 | if (error) { 30 | console.log('No language file found for language ' + lang + '. Defaulting to en_US'); 31 | lang = 'en_US'; 32 | } 33 | 34 | fs.readFile(langFilePath, function (error, data) { 35 | if (error) { 36 | throw error; 37 | } 38 | 39 | try { 40 | data = JSON.parse(data); 41 | } catch (e) { 42 | throw e; // TODO: do something better with the error here 43 | } 44 | 45 | ghost.polyglot().extend(data); 46 | 47 | next(); 48 | }); 49 | }); 50 | }; 51 | }; 52 | 53 | 54 | module.exports.load = I18n; -------------------------------------------------------------------------------- /core/server/storage/base.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | moment = require('moment'), 3 | path = require('path'), 4 | when = require('when'), 5 | baseStore; 6 | 7 | // TODO: would probably be better to put these on the prototype and have proper constructors etc 8 | baseStore = { 9 | 'getTargetDir': function (baseDir) { 10 | var m = moment(new Date().getTime()), 11 | month = m.format('MMM'), 12 | year = m.format('YYYY'); 13 | 14 | if (baseDir) { 15 | return path.join(baseDir, year, month); 16 | } 17 | 18 | return path.join(year, month); 19 | }, 20 | 'generateUnique': function (store, dir, name, ext, i, done) { 21 | var self = this, 22 | filename, 23 | append = ''; 24 | 25 | if (i) { 26 | append = '-' + i; 27 | } 28 | 29 | filename = path.join(dir, name + append + ext); 30 | 31 | store.exists(filename).then(function (exists) { 32 | if (exists) { 33 | setImmediate(function () { 34 | i = i + 1; 35 | self.generateUnique(store, dir, name, ext, i, done); 36 | }); 37 | } else { 38 | done.resolve(filename); 39 | } 40 | }); 41 | }, 42 | 'getUniqueFileName': function (store, image, targetDir) { 43 | var done = when.defer(), 44 | ext = path.extname(image.name), 45 | name = path.basename(image.name, ext).replace(/[\W]/gi, '_'); 46 | 47 | this.generateUnique(store, targetDir, name, ext, 0, done); 48 | 49 | return done.promise; 50 | } 51 | }; 52 | 53 | module.exports = baseStore; -------------------------------------------------------------------------------- /core/client/helpers/index.js: -------------------------------------------------------------------------------- 1 | /*globals Handlebars, moment, Ghost */ 2 | (function () { 3 | 'use strict'; 4 | Handlebars.registerHelper('date', function (context, options) { 5 | if (!options && context.hasOwnProperty('hash')) { 6 | options = context; 7 | context = undefined; 8 | 9 | // set to published_at by default, if it's available 10 | // otherwise, this will print the current date 11 | if (this.published_at) { 12 | context = this.published_at; 13 | } 14 | } 15 | 16 | // ensure that context is undefined, not null, as that can cause errors 17 | context = context === null ? undefined : context; 18 | 19 | var f = options.hash.format || 'MMM Do, YYYY', 20 | timeago = options.hash.timeago, 21 | date; 22 | 23 | 24 | if (timeago) { 25 | date = moment(context).fromNow(); 26 | } else { 27 | date = moment(context).format(f); 28 | } 29 | return date; 30 | }); 31 | 32 | Handlebars.registerHelper('adminUrl', function () { 33 | return Ghost.paths.subdir + '/ghost'; 34 | }); 35 | 36 | Handlebars.registerHelper('asset', function (context, options) { 37 | var output = '', 38 | isAdmin = options && options.hash && options.hash.ghost; 39 | 40 | output += Ghost.paths.subdir + '/'; 41 | 42 | if (!context.match(/^shared/)) { 43 | if (isAdmin) { 44 | output += 'ghost/'; 45 | } else { 46 | output += 'assets/'; 47 | } 48 | } 49 | 50 | output += context; 51 | return new Handlebars.SafeString(output); 52 | }); 53 | }()); 54 | -------------------------------------------------------------------------------- /core/test/functional/frontend/post_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests the default post page 3 | */ 4 | 5 | /*globals CasperTest, casper, __utils__, url, testPost, falseUser, email */ 6 | 7 | // Tests when permalinks is set to date 8 | CasperTest.begin('Post page does not load as slug', 2, function suite(test) { 9 | CasperTest.Routines.togglePermalinks.run('on'); 10 | casper.thenOpen(url + 'welcome-to-ghost', function then(response) { 11 | test.assertTitle('404 — Page Not Found', 'The post should return 404 page'); 12 | test.assertElementCount('.content .post', 0, 'There is no post on this page'); 13 | }); 14 | CasperTest.Routines.togglePermalinks.run('off'); 15 | }, false); 16 | 17 | CasperTest.begin('Post page loads', 3, function suite(test) { 18 | casper.thenOpen(url + 'welcome-to-ghost', function then(response) { 19 | test.assertTitle('Welcome to Ghost', 'The post should have a title and it should be "Welcome to Ghost"'); 20 | test.assertElementCount('.content .post', 1, 'There is exactly one post on this page'); 21 | test.assertSelectorHasText('.poweredby', 'Proudly published with Ghost'); 22 | }); 23 | }, true); 24 | 25 | CasperTest.begin('Test helpers on welcome post', 4, function suite(test) { 26 | casper.start(url + 'welcome-to-ghost', function then(response) { 27 | // body class 28 | test.assertExists('body.post-template', 'body_class outputs correct post-template class'); 29 | test.assertExists('body.tag-getting-started', 'body_class outputs correct tag class'); 30 | // post class 31 | test.assertExists('article.post', 'post_class outputs correct post class'); 32 | test.assertExists('article.tag-getting-started', 'post_class outputs correct tag class'); 33 | }); 34 | }, true); -------------------------------------------------------------------------------- /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(150); 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(150); 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/client/assets/sass/modules/breakpoint/parsers/resolution/_resolution.scss: -------------------------------------------------------------------------------- 1 | @function breakpoint-make-resolutions($resolution) { 2 | $length: length($resolution); 3 | 4 | $output: (); 5 | 6 | @if $length == 2 { 7 | $feature: ''; 8 | $value: ''; 9 | 10 | // Find which is number 11 | @if type-of(nth($resolution, 1)) == 'number' { 12 | $value: nth($resolution, 1); 13 | } 14 | @else { 15 | $value: nth($resolution, 2); 16 | } 17 | 18 | // Determine min/max/standard 19 | @if index($resolution, 'min-resolution') { 20 | $feature: 'min-'; 21 | } 22 | @else if index($resolution, 'max-resolution') { 23 | $feature: 'max-'; 24 | } 25 | 26 | $standard: '(#{$feature}resolution: #{$value})'; 27 | 28 | // If we're not dealing with dppx, 29 | @if unit($value) != 'dppx' { 30 | $base: 96dpi; 31 | @if unit($value) == 'dpcm' { 32 | $base: 243.84dpcm; 33 | } 34 | // Write out feature tests 35 | $webkit: ''; 36 | $moz: ''; 37 | $webkit: '(-webkit-#{$feature}device-pixel-ratio: #{$value / $base})'; 38 | $moz: '(#{$feature}-moz-device-pixel-ratio: #{$value / $base})'; 39 | // Append to output 40 | $output: append($output, $standard, space); 41 | $output: append($output, $webkit, space); 42 | $output: append($output, $moz, space); 43 | } 44 | @else { 45 | $webkit: ''; 46 | $moz: ''; 47 | $webkit: '(-webkit-#{$feature}device-pixel-ratio: #{$value / 1dppx})'; 48 | $moz: '(#{$feature}-moz-device-pixel-ratio: #{$value / 1dppx})'; 49 | $fallback: '(#{$feature}resolution: #{$value / 1dppx * 96dpi})'; 50 | // Append to output 51 | $output: append($output, $standard, space); 52 | $output: append($output, $webkit, space); 53 | $output: append($output, $moz, space); 54 | $output: append($output, $fallback, space); 55 | } 56 | 57 | } 58 | 59 | @return $output; 60 | } 61 | -------------------------------------------------------------------------------- /core/client/assets/sass/modules/breakpoint/_respond-to.scss: -------------------------------------------------------------------------------- 1 | //////////////////////// 2 | // Default the Breakpoints variable 3 | //////////////////////// 4 | $breakpoints: () !default; 5 | 6 | //////////////////////// 7 | // Respond-to API Mixin 8 | //////////////////////// 9 | 10 | @mixin respond-to($context, $no-query: false) { 11 | @if type-of($breakpoints) != 'list' { 12 | // Just in case someone writes gibberish to the $breakpoints variable. 13 | @warn "Your breakpoints aren't a list! See https://github.com/snugug/respond-to#api if you'd like a reminder on how to use Respond-to"; 14 | } 15 | @if length($breakpoints) != 0 { 16 | // If there's only one breakpoint, SASS will think it's a space separated list :P 17 | @if length($breakpoints) == 2 and type-of(nth($breakpoints, 1)) != 'list' { 18 | $breakpoints: append((), (nth($breakpoints, 1), nth($breakpoints, 2))); 19 | } 20 | @each $bkpt in $breakpoints { 21 | @if $context == nth($bkpt, 1) { 22 | $length: length($bkpt); 23 | $mq: false !default; 24 | 25 | @for $i from 2 through $length { 26 | // If it's the first item, override $mq 27 | @if $i == 2 { 28 | $mq: nth($bkpt, $i); 29 | } 30 | // Else, join $mq 31 | @else { 32 | $mq: join($mq, nth($bkpt, $i)); 33 | } 34 | } 35 | 36 | @include breakpoint($mq, $no-query) { 37 | @content; 38 | } 39 | } 40 | } 41 | } 42 | @else { 43 | @warn "You haven't created any breakpoints yet! Make some already! See https://github.com/snugug/respond-to#api if you'd like a reminder on how to use Respond-to"; 44 | @content; 45 | } 46 | } 47 | 48 | ////////////////////////////// 49 | // Add Breakpoint to Breakpoints 50 | ////////////////////////////// 51 | @function add-breakpoint($name, $bkpt) { 52 | $bkpt: $name $bkpt; 53 | $output: append($breakpoints, $bkpt, 'comma'); 54 | @return $output; 55 | } -------------------------------------------------------------------------------- /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/server/data/default-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": { 3 | "databaseVersion": { 4 | "defaultValue": "001" 5 | }, 6 | "dbHash": { 7 | "defaultValue": null 8 | }, 9 | "nextUpdateCheck": { 10 | "defaultValue": null 11 | }, 12 | "displayUpdateNotification": { 13 | "defaultValue": false 14 | } 15 | }, 16 | "blog": { 17 | "title": { 18 | "defaultValue": "Ghost" 19 | }, 20 | "description": { 21 | "defaultValue": "Just a blogging platform." 22 | }, 23 | "email": { 24 | "defaultValue": "ghost@example.com", 25 | "validations": { 26 | "notNull": true, 27 | "isEmail": true 28 | } 29 | }, 30 | "logo": { 31 | "defaultValue": "" 32 | }, 33 | "cover": { 34 | "defaultValue": "" 35 | }, 36 | "defaultLang": { 37 | "defaultValue": "en_US", 38 | "validations": { 39 | "notNull": true 40 | } 41 | }, 42 | "postsPerPage": { 43 | "defaultValue": "6", 44 | "validations": { 45 | "notNull": true, 46 | "isInt": true, 47 | "max": 1000 48 | } 49 | }, 50 | "forceI18n": { 51 | "defaultValue": "true", 52 | "validations": { 53 | "notNull": true, 54 | "isIn": ["true", "false"] 55 | } 56 | }, 57 | "permalinks": { 58 | "defaultValue": "/:slug/", 59 | "validations": { 60 | "is": "^(/:?[a-z]+){1,}/$", 61 | "regex": "(:id|:slug)", 62 | "notContains": "/ghost/" 63 | } 64 | } 65 | }, 66 | "theme": { 67 | "activeTheme": { 68 | "defaultValue": "casper" 69 | } 70 | }, 71 | "plugin": { 72 | "activePlugins": { 73 | "defaultValue": "[]" 74 | }, 75 | "installedPlugins": { 76 | "defaultValue": "[]" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /core/client/models/post.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, $, _, Backbone */ 2 | (function () { 3 | 'use strict'; 4 | 5 | Ghost.Models.Post = Ghost.ProgressModel.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.paths.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('../utils'), 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 | testUtils.initData().then(function () { 26 | done(); 27 | }, done); 28 | }); 29 | 30 | afterEach(function (done) { 31 | testUtils.clearData().then(function () { 32 | done(); 33 | }, done); 34 | }); 35 | 36 | it("exports data", function (done) { 37 | // Stub migrations to return 000 as the current database version 38 | var migrationStub = sinon.stub(migration, "getDatabaseVersion", function () { 39 | return when.resolve("001"); 40 | }); 41 | 42 | exporter().then(function (exportData) { 43 | var tables = ['posts', 'users', 'roles', 'roles_users', 'permissions', 'permissions_roles', 'permissions_users', 44 | 'settings', 'tags', 'posts_tags']; 45 | 46 | should.exist(exportData); 47 | 48 | should.exist(exportData.meta); 49 | should.exist(exportData.data); 50 | 51 | exportData.meta.version.should.equal("001"); 52 | _.findWhere(exportData.data.settings, {key: "databaseVersion"}).value.should.equal("001"); 53 | 54 | _.each(tables, function (name) { 55 | should.exist(exportData.data[name]); 56 | }); 57 | // should not export sqlite data 58 | should.not.exist(exportData.data.sqlite_sequence); 59 | 60 | migrationStub.restore(); 61 | done(); 62 | }).then(null, done); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /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 | /*jslint unparam:true*/ 45 | if (result.isDirectory()) { 46 | fileDeferred.resolve(readDir(fpath, options, depth + 1)); 47 | } else { 48 | fileDeferred.resolve(fpath); 49 | } 50 | }); 51 | }); 52 | 53 | return keys.all(subtree).then(function (theFiles) { 54 | return treeDeferred.resolve(theFiles); 55 | }); 56 | }); 57 | 58 | return when(treePromise).then(function (prom) { 59 | return prom; 60 | }); 61 | }, 62 | readAll = function (dir, options, depth) { 63 | return when(readDir(dir, options, depth)).then(function (paths) { 64 | return paths; 65 | }); 66 | }; 67 | 68 | module.exports = readAll; 69 | -------------------------------------------------------------------------------- /core/server/routes/api.js: -------------------------------------------------------------------------------- 1 | var middleware = require('../middleware').middleware, 2 | api = require('../api'); 3 | 4 | module.exports = function (server) { 5 | // ### API routes 6 | /* TODO: auth should be public auth not user auth */ 7 | // #### Posts 8 | server.get('/ghost/api/v0.1/posts', middleware.authAPI, api.requestHandler(api.posts.browse)); 9 | server.post('/ghost/api/v0.1/posts', middleware.authAPI, api.requestHandler(api.posts.add)); 10 | server.get('/ghost/api/v0.1/posts/:id', middleware.authAPI, api.requestHandler(api.posts.read)); 11 | server.put('/ghost/api/v0.1/posts/:id', middleware.authAPI, api.requestHandler(api.posts.edit)); 12 | server.del('/ghost/api/v0.1/posts/:id', middleware.authAPI, api.requestHandler(api.posts.destroy)); 13 | server.get('/ghost/api/v0.1/posts/getSlug/:title', middleware.authAPI, api.requestHandler(api.posts.getSlug)); 14 | // #### Settings 15 | server.get('/ghost/api/v0.1/settings/', middleware.authAPI, api.requestHandler(api.settings.browse)); 16 | server.get('/ghost/api/v0.1/settings/:key/', middleware.authAPI, api.requestHandler(api.settings.read)); 17 | server.put('/ghost/api/v0.1/settings/', middleware.authAPI, api.requestHandler(api.settings.edit)); 18 | // #### Users 19 | server.get('/ghost/api/v0.1/users/', middleware.authAPI, api.requestHandler(api.users.browse)); 20 | server.get('/ghost/api/v0.1/users/:id/', middleware.authAPI, api.requestHandler(api.users.read)); 21 | server.put('/ghost/api/v0.1/users/:id/', middleware.authAPI, api.requestHandler(api.users.edit)); 22 | // #### Tags 23 | server.get('/ghost/api/v0.1/tags/', middleware.authAPI, api.requestHandler(api.tags.all)); 24 | // #### Notifications 25 | server.del('/ghost/api/v0.1/notifications/:id', middleware.authAPI, api.requestHandler(api.notifications.destroy)); 26 | server.post('/ghost/api/v0.1/notifications/', middleware.authAPI, api.requestHandler(api.notifications.add)); 27 | // #### Import/Export 28 | server.get('/ghost/api/v0.1/db/', middleware.auth, api.db.exportContent); 29 | server.post('/ghost/api/v0.1/db/', middleware.authAPI, middleware.busboy, api.requestHandler(api.db.importContent)); 30 | server.del('/ghost/api/v0.1/db/', middleware.authAPI, api.requestHandler(api.db.deleteAllContent)); 31 | }; -------------------------------------------------------------------------------- /core/server/views/debug.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
    3 | 13 |
    14 |
    15 |

    General

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

    Export the blog settings and data.

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

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

    35 |
    36 |
    37 |
    38 |
    39 |
    40 |
    41 | 42 | Delete 43 |

    Delete all posts and tags from the database.

    44 |
    45 |
    46 |
    47 |
    48 |
    49 |
    50 | -------------------------------------------------------------------------------- /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 | 52 |
    53 | {{/if}} 54 |
    55 | 56 | 57 | -------------------------------------------------------------------------------- /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: "\21AA"; 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 | } -------------------------------------------------------------------------------- /core/server/plugins/loader.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require('path'), 3 | _ = require('underscore'), 4 | when = require('when'), 5 | appProxy = require('./proxy'), 6 | config = require('../config'), 7 | loader; 8 | 9 | 10 | 11 | // Get a relative path to the given plugins root, defaults 12 | // to be relative to __dirname 13 | function getPluginRelativePath(name, relativeTo) { 14 | relativeTo = relativeTo || __dirname; 15 | 16 | return path.relative(relativeTo, path.join(config.paths().pluginPath, name)); 17 | } 18 | 19 | 20 | function getPluginByName(name) { 21 | // Grab the plugin class to instantiate 22 | var PluginClass = require(getPluginRelativePath(name)), 23 | plugin; 24 | 25 | // Check for an actual class, otherwise just use whatever was returned 26 | if (_.isFunction(PluginClass)) { 27 | plugin = new PluginClass(appProxy); 28 | } else { 29 | plugin = PluginClass; 30 | } 31 | 32 | return plugin; 33 | } 34 | 35 | // The loader is responsible for loading plugins 36 | loader = { 37 | // Load a plugin and return the instantiated plugin 38 | installPluginByName: function (name) { 39 | var plugin = getPluginByName(name); 40 | 41 | // Check for an install() method on the plugin. 42 | if (!_.isFunction(plugin.install)) { 43 | return when.reject(new Error("Error loading plugin named " + name + "; no install() method defined.")); 44 | } 45 | 46 | // Wrapping the install() with a when because it's possible 47 | // to not return a promise from it. 48 | return when(plugin.install(appProxy)).then(function () { 49 | return when.resolve(plugin); 50 | }); 51 | }, 52 | 53 | // Activate a plugin and return it 54 | activatePluginByName: function (name) { 55 | var plugin = getPluginByName(name); 56 | 57 | // Check for an activate() method on the plugin. 58 | if (!_.isFunction(plugin.activate)) { 59 | return when.reject(new Error("Error loading plugin named " + name + "; no activate() method defined.")); 60 | } 61 | 62 | // Wrapping the activate() with a when because it's possible 63 | // to not return a promise from it. 64 | return when(plugin.activate(appProxy)).then(function () { 65 | return when.resolve(plugin); 66 | }); 67 | } 68 | }; 69 | 70 | module.exports = loader; -------------------------------------------------------------------------------- /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/utils/api.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | url = require('url'), 3 | ApiRouteBase = '/ghost/api/v0.1/', 4 | host = 'localhost', 5 | port = '2369'; 6 | schema = "http://", 7 | expectedProperties = { 8 | posts: ['posts', 'page', 'limit', 'pages', 'total'], 9 | post: ['id', 'uuid', 'title', 'slug', 'markdown', 'html', 'meta_title', 'meta_description', 10 | 'featured', 'image', 'status', 'language', 'author_id', 'created_at', 'created_by', 'updated_at', 11 | 'updated_by', 'published_at', 'published_by', 'page', 'author', 'user', 'tags'], 12 | // TODO: remove databaseVersion, dbHash 13 | settings: ['databaseVersion', 'dbHash', 'title', 'description', 'email', 'logo', 'cover', 'defaultLang', 14 | "permalinks", 'postsPerPage', 'forceI18n', 'activeTheme', 'activePlugins', 'installedPlugins', 15 | 'availableThemes', 'nextUpdateCheck', 'displayUpdateNotification'], 16 | tag: ['id', 'uuid', 'name', 'slug', 'description', 'parent_id', 17 | 'meta_title', 'meta_description', 'created_at', 'created_by', 'updated_at', 'updated_by'], 18 | user: ['id', 'uuid', 'name', 'slug', 'email', 'image', 'cover', 'bio', 'website', 19 | 'location', 'accessibility', 'status', 'language', 'meta_title', 'meta_description', 20 | 'created_at', 'updated_at'] 21 | }; 22 | 23 | 24 | function getApiURL (route) { 25 | var baseURL = url.resolve(schema + host + ':' + port, ApiRouteBase); 26 | return url.resolve(baseURL, route); 27 | } 28 | function getSigninURL () { 29 | return url.resolve(schema + host + ':' + port, 'ghost/signin/'); 30 | } 31 | function getAdminURL () { 32 | return url.resolve(schema + host + ':' + port, 'ghost/'); 33 | } 34 | 35 | // make sure the API only returns expected properties only 36 | function checkResponse (jsonResponse, objectType) { 37 | checkResponseValue(jsonResponse, expectedProperties[objectType]); 38 | } 39 | function checkResponseValue (jsonResponse, properties) { 40 | Object.keys(jsonResponse).length.should.eql(properties.length); 41 | for(var i=0; i\})?!(?:\[([^\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 | // 4 or more inline underscores e.g. Ghost rocks my _____! 28 | { 29 | type: 'lang', 30 | filter: function (text) { 31 | return text.replace(/([^_\n\r])(_{4,})/g, function (match, prefix, underscores) { 32 | return prefix + underscores.replace(/_/g, '_'); 33 | }); 34 | } 35 | } 36 | ]; 37 | }; 38 | 39 | // Client-side export 40 | if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) { 41 | window.Showdown.extensions.ghostdown = ghostdown; 42 | } 43 | // Server-side export 44 | if (typeof module !== 'undefined') module.exports = ghostdown; 45 | }()); 46 | -------------------------------------------------------------------------------- /core/test/functional/api/tags_test.js: -------------------------------------------------------------------------------- 1 | /*globals describe, before, beforeEach, afterEach, it */ 2 | var testUtils = require('../../utils'), 3 | should = require('should'), 4 | _ = require('underscore'), 5 | request = require('request'); 6 | 7 | request = request.defaults({jar:true}) 8 | 9 | describe('Tag API', function () { 10 | 11 | var user = testUtils.DataGenerator.forModel.users[0], 12 | csrfToken = ''; 13 | 14 | before(function (done) { 15 | testUtils.clearData() 16 | .then(function () { 17 | return testUtils.initData(); 18 | }) 19 | .then(function () { 20 | return testUtils.insertDefaultFixtures(); 21 | }) 22 | .then(function () { 23 | request.get(testUtils.API.getSigninURL(), function (error, response, body) { 24 | response.should.have.status(200); 25 | var pattern_meta = //i; 26 | pattern_meta.should.exist; 27 | csrfToken = body.match(pattern_meta)[1]; 28 | setTimeout((function () { 29 | request.post({uri: testUtils.API.getSigninURL(), 30 | headers: {'X-CSRF-Token': csrfToken}}, function (error, response, body) { 31 | response.should.have.status(200); 32 | request.get(testUtils.API.getAdminURL(), function (error, response, body) { 33 | response.should.have.status(200); 34 | csrfToken = body.match(pattern_meta)[1]; 35 | done(); 36 | }); 37 | }).form({email: user.email, password: user.password}); 38 | }), 2000); 39 | }); 40 | }, done); 41 | }); 42 | 43 | it('can retrieve all tags', function (done) { 44 | request.get(testUtils.API.getApiURL('tags/'), function (error, response, body) { 45 | response.should.have.status(200); 46 | should.not.exist(response.headers['x-cache-invalidate']); 47 | response.should.be.json; 48 | var jsonResponse = JSON.parse(body); 49 | jsonResponse.should.exist; 50 | jsonResponse.should.have.length(5); 51 | testUtils.API.checkResponse(jsonResponse[0], 'tag'); 52 | done(); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /core/server/middleware/ghost-busboy.js: -------------------------------------------------------------------------------- 1 | var BusBoy = require('busboy'), 2 | fs = require('fs-extra'), 3 | path = require('path'), 4 | os = require('os'), 5 | crypto = require('crypto'); 6 | 7 | // ### ghostBusboy 8 | // Process multipart file streams 9 | function ghostBusBoy(req, res, next) { 10 | var busboy, 11 | stream, 12 | tmpDir, 13 | hasError = false; 14 | 15 | // busboy is only used for POST requests 16 | if (req.method && !req.method.match(/post/i)) { 17 | return next(); 18 | } 19 | 20 | busboy = new BusBoy({ headers: req.headers }); 21 | tmpDir = os.tmpdir(); 22 | 23 | req.files = req.files || {}; 24 | req.body = req.body || {}; 25 | 26 | busboy.on('file', function (fieldname, file, filename, encoding, mimetype) { 27 | var filePath, 28 | tmpFileName, 29 | md5 = crypto.createHash('md5'); 30 | 31 | // If the filename is invalid, mark an error 32 | if (!filename) { 33 | hasError = true; 34 | } 35 | // If we've flagged any errors, do not process any streams 36 | if (hasError) { 37 | return file.emit('end'); 38 | } 39 | 40 | // Create an MD5 hash of original filename 41 | md5.update(filename, 'utf8'); 42 | 43 | tmpFileName = (new Date()).getTime() + md5.digest('hex'); 44 | 45 | filePath = path.join(tmpDir, tmpFileName || 'temp.tmp'); 46 | 47 | file.on('end', function () { 48 | req.files[fieldname] = { 49 | type: mimetype, 50 | encoding: encoding, 51 | name: filename, 52 | path: filePath 53 | }; 54 | }); 55 | 56 | busboy.on('limit', function () { 57 | hasError = true; 58 | res.send(413, { errorCode: 413, message: 'File size limit breached.' }); 59 | }); 60 | 61 | busboy.on('error', function (error) { 62 | console.log('Error', 'Something went wrong uploading the file', error); 63 | }); 64 | 65 | stream = fs.createWriteStream(filePath); 66 | 67 | stream.on('error', function (error) { 68 | console.log('Error', 'Something went wrong uploading the file', error); 69 | }); 70 | 71 | file.pipe(stream); 72 | 73 | }); 74 | 75 | busboy.on('field', function (fieldname, val) { 76 | req.body[fieldname] = val; 77 | }); 78 | 79 | busboy.on('end', function () { 80 | next(); 81 | }); 82 | 83 | req.pipe(busboy); 84 | } 85 | 86 | module.exports = ghostBusBoy; -------------------------------------------------------------------------------- /core/test/unit/plugin_proxy_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, beforeEach, afterEach, before, it*/ 2 | var should = require('should'), 3 | sinon = require('sinon'), 4 | _ = require("underscore"), 5 | helpers = require('../../server/helpers'), 6 | filters = require('../../server/filters'), 7 | 8 | // Stuff we are testing 9 | appProxy = require('../../server/plugins/proxy'); 10 | 11 | describe('App Proxy', function () { 12 | 13 | var sandbox, 14 | fakeApi; 15 | 16 | beforeEach(function () { 17 | sandbox = sinon.sandbox.create(); 18 | 19 | fakeApi = { 20 | posts: { 21 | browse: sandbox.stub(), 22 | read: sandbox.stub(), 23 | edit: sandbox.stub(), 24 | add: sandbox.stub(), 25 | destroy: sandbox.stub() 26 | }, 27 | users: { 28 | browse: sandbox.stub(), 29 | read: sandbox.stub(), 30 | edit: sandbox.stub() 31 | }, 32 | tags: { 33 | all: sandbox.stub() 34 | }, 35 | notifications: { 36 | destroy: sandbox.stub(), 37 | add: sandbox.stub() 38 | }, 39 | settings: { 40 | browse: sandbox.stub(), 41 | read: sandbox.stub(), 42 | add: sandbox.stub() 43 | } 44 | }; 45 | }); 46 | 47 | afterEach(function () { 48 | sandbox.restore(); 49 | }); 50 | 51 | it('creates a ghost proxy', function () { 52 | should.exist(appProxy.filters); 53 | appProxy.filters.register.should.equal(filters.registerFilter); 54 | appProxy.filters.unregister.should.equal(filters.unregisterFilter); 55 | 56 | should.exist(appProxy.helpers); 57 | appProxy.helpers.register.should.equal(helpers.registerThemeHelper); 58 | appProxy.helpers.registerAsync.should.equal(helpers.registerAsyncThemeHelper); 59 | 60 | should.exist(appProxy.api); 61 | 62 | should.exist(appProxy.api.posts); 63 | should.not.exist(appProxy.api.posts.edit); 64 | should.not.exist(appProxy.api.posts.add); 65 | should.not.exist(appProxy.api.posts.destroy); 66 | 67 | should.not.exist(appProxy.api.users); 68 | 69 | should.exist(appProxy.api.tags); 70 | 71 | should.exist(appProxy.api.notifications); 72 | should.not.exist(appProxy.api.notifications.destroy); 73 | 74 | should.exist(appProxy.api.settings); 75 | should.not.exist(appProxy.api.settings.browse); 76 | should.not.exist(appProxy.api.settings.add); 77 | 78 | }); 79 | }); -------------------------------------------------------------------------------- /core/server/storage/localfilesystem.js: -------------------------------------------------------------------------------- 1 | // # Local File System Image Storage module 2 | // The (default) module for storing images, using the local file system 3 | 4 | var _ = require('underscore'), 5 | express = require('express'), 6 | fs = require('fs-extra'), 7 | nodefn = require('when/node/function'), 8 | path = require('path'), 9 | when = require('when'), 10 | errors = require('../errorHandling'), 11 | configPaths = require('../config/paths'), 12 | baseStore = require('./base'), 13 | 14 | localFileStore; 15 | 16 | localFileStore = _.extend(baseStore, { 17 | // ### Save 18 | // Saves the image to storage (the file system) 19 | // - image is the express image object 20 | // - returns a promise which ultimately returns the full url to the uploaded image 21 | 'save': function (image) { 22 | var saved = when.defer(), 23 | targetDir = this.getTargetDir(configPaths().imagesRelPath), 24 | targetFilename; 25 | 26 | this.getUniqueFileName(this, image, targetDir).then(function (filename) { 27 | targetFilename = filename; 28 | return nodefn.call(fs.mkdirs, targetDir); 29 | }).then(function () { 30 | return nodefn.call(fs.copy, image.path, targetFilename); 31 | }).then(function () { 32 | return nodefn.call(fs.unlink, image.path).otherwise(errors.logError); 33 | }).then(function () { 34 | // The src for the image must be in URI format, not a file system path, which in Windows uses \ 35 | // For local file system storage can use relative path so add a slash 36 | var fullUrl = (configPaths().subdir + '/' + targetFilename).replace(new RegExp('\\' + path.sep, 'g'), '/'); 37 | return saved.resolve(fullUrl); 38 | }).otherwise(function (e) { 39 | errors.logError(e); 40 | return saved.reject(e); 41 | }); 42 | 43 | return saved.promise; 44 | }, 45 | 46 | 'exists': function (filename) { 47 | // fs.exists does not play nicely with nodefn because the callback doesn't have an error argument 48 | var done = when.defer(); 49 | 50 | fs.exists(filename, function (exists) { 51 | done.resolve(exists); 52 | }); 53 | 54 | return done.promise; 55 | }, 56 | 57 | // middleware for serving the files 58 | 'serve': function () { 59 | var ONE_HOUR_MS = 60 * 60 * 1000, 60 | ONE_YEAR_MS = 365 * 24 * ONE_HOUR_MS; 61 | 62 | // For some reason send divides the max age number by 1000 63 | return express['static'](configPaths().imagesPath, {maxAge: ONE_YEAR_MS}); 64 | } 65 | }); 66 | 67 | module.exports = localFileStore; 68 | -------------------------------------------------------------------------------- /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/test/functional/frontend/feed_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests if RSS exists and is working 3 | */ 4 | /*globals url, CasperTest, casper */ 5 | CasperTest.begin('Ensure that RSS is available', 11, function suite(test) { 6 | CasperTest.Routines.togglePermalinks.run('off'); 7 | casper.thenOpen(url + 'rss/', function (response) { 8 | var content = this.getPageContent(), 9 | siteTitle = '<![CDATA[Ghost]]>', 10 | siteDescription = '', 11 | siteUrl = 'http://127.0.0.1:2369/', 12 | postTitle = '', 13 | postStart = 'You\'re live!', 14 | postEnd = 'you think :)

    ]]>
    ', 15 | postLink = 'http://127.0.0.1:2369/welcome-to-ghost/', 16 | postCreator = ''; 17 | 18 | test.assertEqual(response.status, 200, 'Response status should be 200.'); 19 | test.assert(content.indexOf('= 0, 'Feed should contain = 0, 'Feed should contain blog title.'); 21 | test.assert(content.indexOf(siteDescription) >= 0, 'Feed should contain blog description.'); 22 | test.assert(content.indexOf(siteUrl) >= 0, 'Feed should contain link to blog.'); 23 | test.assert(content.indexOf(postTitle) >= 0, 'Feed should contain welcome post title.'); 24 | test.assert(content.indexOf(postStart) >= 0, 'Feed should contain start of welcome post content.'); 25 | test.assert(content.indexOf(postEnd) >= 0, 'Feed should contain end of welcome post content.'); 26 | test.assert(content.indexOf(postLink) >= 0, 'Feed should have link to the welcome post.'); 27 | test.assert(content.indexOf(postCreator) >= 0, 'Welcome post should have Test User as the creator.'); 28 | test.assert(content.indexOf('') >= 0, 'Feed should contain '); 29 | }); 30 | }, false); 31 | 32 | CasperTest.begin('Ensures dated permalinks works with RSS', 2, function suite(test) { 33 | CasperTest.Routines.togglePermalinks.run('on'); 34 | casper.thenOpen(url + 'rss/', function (response) { 35 | var content = this.getPageContent(), 36 | today = new Date(), 37 | dd = ("0" + today.getDate()).slice(-2), 38 | mm = ("0" + (today.getMonth() + 1)).slice(-2), 39 | yyyy = today.getFullYear(), 40 | postLink = '/' + yyyy + '/' + mm + '/' + dd + '/welcome-to-ghost/'; 41 | 42 | test.assertEqual(response.status, 200, 'Response status should be 200.'); 43 | test.assert(content.indexOf(postLink) >= 0, 'Feed should have dated permalink.'); 44 | }); 45 | CasperTest.Routines.togglePermalinks.run('off'); 46 | }, false); 47 | -------------------------------------------------------------------------------- /core/server/views/default.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Ghost Admin 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {{{block "pageStyles"}}} 33 | 34 | 35 | {{#unless hideNavbar}} 36 | {{> navbar}} 37 | {{/unless}} 38 | 39 |
    40 | {{updateNotification}} 41 | 42 | 45 | 46 | {{{body}}} 47 | 48 |
    49 |

    Hello There! Looks like something went wrong with your JavaScript.

    50 |

    Either you need to enable JavaScript, or you haven't built the JavaScript files yet. See the README and CONTRIBUTING files for more info.

    51 |
    52 |
    53 | 54 | 56 | 57 | 58 | {{{ghostScriptTags}}} 59 | 60 | {{{block "bodyScripts"}}} 61 | 62 | 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "ghost", 3 | "version" : "0.4.0", 4 | "description" : "Just a blogging platform.", 5 | "author" : "Ghost Foundation", 6 | "homepage" : "http://ghost.org", 7 | "keywords" : [ 8 | "ghost", 9 | "blog", 10 | "cms" 11 | ], 12 | "repository" : { 13 | "type": "git", 14 | "url": "git://github.com/TryGhost/Ghost.git" 15 | }, 16 | "bugs" : "https://github.com/TryGhost/Ghost/issues", 17 | "contributors": "https://github.com/TryGhost/Ghost/graphs/contributors", 18 | "private" : true, 19 | "licenses" : [ 20 | { 21 | "type": "MIT", 22 | "url": "https://raw.github.com/TryGhost/Ghost/master/LICENSE" 23 | } 24 | ], 25 | "main": "./core/index", 26 | "scripts": { 27 | "start": "node index", 28 | "test": "grunt validate --verbose" 29 | }, 30 | "engines": { 31 | "node": "~0.10.0" 32 | }, 33 | "engineStrict": true, 34 | "dependencies": { 35 | "bcryptjs": "0.7.10", 36 | "bookshelf": "0.6.1", 37 | "busboy": "0.0.12", 38 | "colors": "0.6.2", 39 | "connect-slashes": "1.2.0", 40 | "downsize": "0.0.4", 41 | "express": "3.4.6", 42 | "express-hbs": "0.5.2", 43 | "fs-extra": "0.8.1", 44 | "knex": "0.5.0", 45 | "moment": "2.4.0", 46 | "node-polyglot": "0.3.0", 47 | "node-uuid": "1.4.1", 48 | "nodemailer": "0.5.13", 49 | "rss": "0.2.1", 50 | "semver": "2.2.1", 51 | "showdown": "0.3.1", 52 | "sqlite3": "2.1.19", 53 | "underscore": "1.5.2", 54 | "unidecode": "0.1.3", 55 | "validator": "1.4.0", 56 | "when": "2.7.0" 57 | }, 58 | "optionalDependencies": { 59 | "mysql": "2.0.0-alpha9" 60 | }, 61 | "devDependencies": { 62 | "blanket": "~1.1.5", 63 | "grunt": "~0.4.1", 64 | "grunt-contrib-clean": "~0.5.0", 65 | "grunt-contrib-compress": "~0.5.2", 66 | "grunt-contrib-concat": "~0.3.0", 67 | "grunt-contrib-copy": "~0.4.1", 68 | "grunt-contrib-handlebars": "~0.6.0", 69 | "grunt-contrib-sass": "~0.5.0", 70 | "grunt-contrib-uglify": "~0.2.5", 71 | "grunt-contrib-watch": "~0.5.3", 72 | "grunt-express-server": "~0.4.11", 73 | "grunt-groc": "~0.4.0", 74 | "grunt-jslint": "~1.1.1", 75 | "grunt-mocha-cli": "~1.4.0", 76 | "grunt-shell": "~0.6.1", 77 | "grunt-update-submodules": "~0.2.1", 78 | "matchdep": "~0.3.0", 79 | "mocha": "~1.15.1", 80 | "rewire": "~2.0.0", 81 | "request": "~2.29.0", 82 | "require-dir": "~0.1.0", 83 | "should": "~2.1.1", 84 | "sinon": "~1.7.3", 85 | "supertest": "~0.8.2" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /core/server/filters.js: -------------------------------------------------------------------------------- 1 | var when = require('when'), 2 | _ = require('underscore'), 3 | 4 | defaults; 5 | 6 | when.pipeline = require('when/pipeline'); 7 | 8 | // ## Default values 9 | /** 10 | * A hash of default values to use instead of 'magic' numbers/strings. 11 | * @type {Object} 12 | */ 13 | defaults = { 14 | filterPriority: 5, 15 | maxPriority: 9 16 | }; 17 | 18 | var Filters = function () { 19 | // Holds the filters 20 | this.filterCallbacks = []; 21 | 22 | // Holds the filter hooks (that are built in to Ghost Core) 23 | this.filters = []; 24 | }; 25 | 26 | // Register a new filter callback function 27 | Filters.prototype.registerFilter = function (name, priority, fn) { 28 | // Curry the priority optional parameter to a default of 5 29 | if (_.isFunction(priority)) { 30 | fn = priority; 31 | priority = defaults.filterPriority; 32 | } 33 | 34 | this.filterCallbacks[name] = this.filterCallbacks[name] || {}; 35 | this.filterCallbacks[name][priority] = this.filterCallbacks[name][priority] || []; 36 | 37 | this.filterCallbacks[name][priority].push(fn); 38 | }; 39 | 40 | // Unregister a filter callback function 41 | Filters.prototype.unregisterFilter = function (name, priority, fn) { 42 | // Curry the priority optional parameter to a default of 5 43 | if (_.isFunction(priority)) { 44 | fn = priority; 45 | priority = defaults.filterPriority; 46 | } 47 | 48 | // Check if it even exists 49 | if (this.filterCallbacks[name] && this.filterCallbacks[name][priority]) { 50 | // Remove the function from the list of filter funcs 51 | this.filterCallbacks[name][priority] = _.without(this.filterCallbacks[name][priority], fn); 52 | } 53 | }; 54 | 55 | // Execute filter functions in priority order 56 | Filters.prototype.doFilter = function (name, args) { 57 | var callbacks = this.filterCallbacks[name], 58 | priorityCallbacks = []; 59 | 60 | // Bug out early if no callbacks by that name 61 | if (!callbacks) { 62 | return when.resolve(args); 63 | } 64 | 65 | // For each priorityLevel 66 | _.times(defaults.maxPriority + 1, function (priority) { 67 | // Add a function that runs its priority level callbacks in a pipeline 68 | priorityCallbacks.push(function (currentArgs) { 69 | // Bug out if no handlers on this priority 70 | if (!_.isArray(callbacks[priority])) { 71 | return when.resolve(currentArgs); 72 | } 73 | 74 | // Call each handler for this priority level, allowing for promises or values 75 | return when.pipeline(callbacks[priority], currentArgs); 76 | }); 77 | }); 78 | 79 | return when.pipeline(priorityCallbacks, args); 80 | }; 81 | 82 | module.exports = new Filters(); 83 | module.exports.Filters = Filters; -------------------------------------------------------------------------------- /core/server/routes/admin.js: -------------------------------------------------------------------------------- 1 | var admin = require('../controllers/admin'), 2 | config = require('../config'), 3 | middleware = require('../middleware').middleware; 4 | 5 | module.exports = function (server) { 6 | var subdir = config.paths().subdir; 7 | // ### Admin routes 8 | /* TODO: put these somewhere in admin */ 9 | server.get('/logout/', function redirect(req, res) { 10 | /*jslint unparam:true*/ 11 | res.redirect(301, subdir + '/ghost/signout/'); 12 | }); 13 | server.get('/signout/', function redirect(req, res) { 14 | /*jslint unparam:true*/ 15 | res.redirect(301, subdir + '/ghost/signout/'); 16 | }); 17 | server.get('/signin/', function redirect(req, res) { 18 | /*jslint unparam:true*/ 19 | res.redirect(301, subdir + '/ghost/signin/'); 20 | }); 21 | server.get('/signup/', function redirect(req, res) { 22 | /*jslint unparam:true*/ 23 | res.redirect(301, subdir + '/ghost/signup/'); 24 | }); 25 | server.get('/ghost/login/', function redirect(req, res) { 26 | /*jslint unparam:true*/ 27 | res.redirect(301, subdir + '/ghost/signin/'); 28 | }); 29 | 30 | server.get('/ghost/signout/', admin.logout); 31 | server.get('/ghost/signin/', middleware.redirectToSignup, middleware.redirectToDashboard, admin.login); 32 | server.get('/ghost/signup/', middleware.redirectToDashboard, admin.signup); 33 | server.get('/ghost/forgotten/', middleware.redirectToDashboard, admin.forgotten); 34 | server.post('/ghost/forgotten/', admin.generateResetToken); 35 | server.get('/ghost/reset/:token', admin.reset); 36 | server.post('/ghost/reset/:token', admin.resetPassword); 37 | server.post('/ghost/signin/', admin.auth); 38 | server.post('/ghost/signup/', admin.doRegister); 39 | server.post('/ghost/changepw/', middleware.auth, admin.changepw); 40 | server.get('/ghost/editor(/:id)/', middleware.auth, admin.editor); 41 | server.get('/ghost/editor/', middleware.auth, admin.editor); 42 | server.get('/ghost/content/', middleware.auth, admin.content); 43 | server.get('/ghost/settings*', middleware.auth, admin.settings); 44 | server.get('/ghost/debug/', middleware.auth, admin.debug.index); 45 | 46 | server.post('/ghost/upload/', middleware.auth, middleware.busboy, admin.uploader); 47 | 48 | // redirect to /ghost and let that do the authentication to prevent redirects to /ghost//admin etc. 49 | server.get(/\/((ghost-admin|admin|wp-admin|dashboard|signin)\/?)$/, function (req, res) { 50 | /*jslint unparam:true*/ 51 | res.redirect(subdir + '/ghost/'); 52 | }); 53 | server.get(/\/(ghost$\/?)/, middleware.auth, function (req, res) { 54 | /*jslint unparam:true*/ 55 | res.redirect(subdir + '/ghost/'); 56 | }); 57 | server.get('/ghost/', middleware.redirectToSignup, middleware.auth, admin.index); 58 | }; -------------------------------------------------------------------------------- /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 | 'reset/:token/' : 'reset' 18 | }, 19 | 20 | signup: function () { 21 | Ghost.currentView = new Ghost.Views.Signup({ el: '.js-signup-box' }); 22 | }, 23 | 24 | login: function () { 25 | Ghost.currentView = new Ghost.Views.Login({ el: '.js-login-box' }); 26 | }, 27 | 28 | forgotten: function () { 29 | Ghost.currentView = new Ghost.Views.Forgotten({ el: '.js-forgotten-box' }); 30 | }, 31 | 32 | reset: function (token) { 33 | Ghost.currentView = new Ghost.Views.ResetPassword({ el: '.js-reset-box', token: token }); 34 | }, 35 | 36 | blog: function () { 37 | var posts = new Ghost.Collections.Posts(); 38 | NProgress.start(); 39 | posts.fetch({ data: { status: 'all', staticPages: 'all'} }).then(function () { 40 | Ghost.currentView = new Ghost.Views.Blog({ el: '#main', collection: posts }); 41 | NProgress.done(); 42 | }); 43 | }, 44 | 45 | settings: function (pane) { 46 | if (!pane) { 47 | // Redirect to settings/general if no pane supplied 48 | this.navigate('/settings/general/', { 49 | trigger: true, 50 | replace: true 51 | }); 52 | return; 53 | } 54 | 55 | // only update the currentView if we don't already have a Settings view 56 | if (!Ghost.currentView || !(Ghost.currentView instanceof Ghost.Views.Settings)) { 57 | Ghost.currentView = new Ghost.Views.Settings({ el: '#main', pane: pane }); 58 | } 59 | }, 60 | 61 | editor: function (id) { 62 | var post = new Ghost.Models.Post(); 63 | post.urlRoot = Ghost.paths.apiRoot + '/posts'; 64 | if (id) { 65 | post.id = id; 66 | post.fetch({ data: {status: 'all'}}).then(function () { 67 | Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post }); 68 | }); 69 | } else { 70 | Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post }); 71 | } 72 | }, 73 | 74 | debug: function () { 75 | Ghost.currentView = new Ghost.Views.Debug({ el: "#main" }); 76 | } 77 | }); 78 | }()); 79 | -------------------------------------------------------------------------------- /core/client/tpl/preview.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 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 | 21 | 24 | 25 | 26 | 29 | 32 | 33 | 34 | 37 | 41 | 42 |
    19 | 20 | 22 | 23 |
    27 | 28 | 30 | 31 |
    35 | Static Page 36 | 38 | 39 | 40 |
    43 |
    44 | Delete This Post 45 |
    46 |
    47 |
    48 |
    49 |

    {{{title}}}

    {{{html}}}
    50 |
    51 | {{#unless title}} 52 |
    53 |
    54 |

    You Haven't Written Any Posts Yet!

    55 |
    56 |
    57 |
    58 | {{/unless}} 59 | -------------------------------------------------------------------------------- /core/client/init.js: -------------------------------------------------------------------------------- 1 | /*globals window, $, _, Backbone, Validator */ 2 | (function () { 3 | 'use strict'; 4 | 5 | function ghostPaths() { 6 | var path = window.location.pathname, 7 | subdir = path.substr(0, path.search('/ghost/')); 8 | 9 | return { 10 | subdir: subdir, 11 | apiRoot: subdir + '/ghost/api/v0.1' 12 | }; 13 | } 14 | 15 | var Ghost = { 16 | Layout : {}, 17 | Views : {}, 18 | Collections : {}, 19 | Models : {}, 20 | Validate : new Validator(), 21 | 22 | paths: ghostPaths(), 23 | 24 | // This is a helper object to denote legacy things in the 25 | // middle of being transitioned. 26 | temporary: {}, 27 | 28 | currentView: null, 29 | router: null 30 | }; 31 | 32 | _.extend(Ghost, Backbone.Events); 33 | 34 | Backbone.oldsync = Backbone.sync; 35 | // override original sync method to make header request contain csrf token 36 | Backbone.sync = function (method, model, options, error) { 37 | options.beforeSend = function (xhr) { 38 | xhr.setRequestHeader('X-CSRF-Token', $("meta[name='csrf-param']").attr('content')); 39 | }; 40 | /* call the old sync method */ 41 | return Backbone.oldsync(method, model, options, error); 42 | }; 43 | 44 | Backbone.oldModelProtoUrl = Backbone.Model.prototype.url; 45 | //overwrite original url method to add slash to end of the url if needed. 46 | Backbone.Model.prototype.url = function () { 47 | var url = Backbone.oldModelProtoUrl.apply(this, arguments); 48 | return url + (url.charAt(url.length - 1) === '/' ? '' : '/'); 49 | }; 50 | 51 | Ghost.init = function () { 52 | // remove the temporary message which appears 53 | $('.js-msg').remove(); 54 | 55 | Ghost.router = new Ghost.Router(); 56 | 57 | // This is needed so Backbone recognizes elements already rendered server side 58 | // as valid views, and events are bound 59 | Ghost.notifications = new Ghost.Views.NotificationCollection({model: []}); 60 | 61 | Backbone.history.start({ 62 | pushState: true, 63 | hashChange: false, 64 | root: Ghost.paths.subdir + '/ghost' 65 | }); 66 | }; 67 | 68 | Ghost.Validate.error = function (object) { 69 | this._errors.push(object); 70 | 71 | return this; 72 | }; 73 | 74 | Ghost.Validate.handleErrors = function () { 75 | Ghost.notifications.clearEverything(); 76 | _.each(Ghost.Validate._errors, function (errorObj) { 77 | 78 | Ghost.notifications.addItem({ 79 | type: 'error', 80 | message: errorObj.message || errorObj, 81 | status: 'passive' 82 | }); 83 | if (errorObj.hasOwnProperty('el')) { 84 | errorObj.el.addClass('input-error'); 85 | } 86 | }); 87 | }; 88 | 89 | window.Ghost = Ghost; 90 | 91 | window.addEventListener("load", Ghost.init, false); 92 | }()); 93 | -------------------------------------------------------------------------------- /core/server/api/index.js: -------------------------------------------------------------------------------- 1 | // # Ghost Data API 2 | // Provides access to the data model 3 | 4 | var _ = require('underscore'), 5 | when = require('when'), 6 | config = require('../config'), 7 | errors = require('../errorHandling'), 8 | db = require('./db'), 9 | settings = require('./settings'), 10 | notifications = require('./notifications'), 11 | posts = require('./posts'), 12 | users = require('./users'), 13 | tags = require('./tags'), 14 | requestHandler, 15 | init; 16 | 17 | // ## Request Handlers 18 | 19 | function cacheInvalidationHeader(req, result) { 20 | var parsedUrl = req._parsedUrl.pathname.replace(/\/$/, '').split('/'), 21 | method = req.method, 22 | endpoint = parsedUrl[4], 23 | id = parsedUrl[5], 24 | cacheInvalidate, 25 | jsonResult = result.toJSON ? result.toJSON() : result; 26 | 27 | if (method === 'POST' || method === 'PUT' || method === 'DELETE') { 28 | if (endpoint === 'settings' || endpoint === 'users' || endpoint === 'db') { 29 | cacheInvalidate = "/*"; 30 | } else if (endpoint === 'posts') { 31 | cacheInvalidate = "/, /page/*, /rss/, /rss/*"; 32 | if (id && jsonResult.slug) { 33 | return config.paths.urlForPost(settings, jsonResult).then(function (postUrl) { 34 | return cacheInvalidate + ', ' + postUrl; 35 | }); 36 | } 37 | } 38 | } 39 | 40 | return when(cacheInvalidate); 41 | } 42 | 43 | // ### requestHandler 44 | // decorator for api functions which are called via an HTTP request 45 | // takes the API method and wraps it so that it gets data from the request and returns a sensible JSON response 46 | requestHandler = function (apiMethod) { 47 | return function (req, res) { 48 | var options = _.extend(req.body, req.files, req.query, req.params), 49 | apiContext = { 50 | user: req.session && req.session.user 51 | }; 52 | 53 | return apiMethod.call(apiContext, options).then(function (result) { 54 | res.json(result || {}); 55 | return cacheInvalidationHeader(req, result).then(function (header) { 56 | if (header) { 57 | res.set({ 58 | "X-Cache-Invalidate": header 59 | }); 60 | } 61 | }); 62 | }, function (error) { 63 | var errorCode = error.errorCode || 500, 64 | errorMsg = {error: _.isString(error) ? error : (_.isObject(error) ? error.message : 'Unknown API Error')}; 65 | res.json(errorCode, errorMsg); 66 | }); 67 | }; 68 | }; 69 | 70 | init = function () { 71 | return settings.updateSettingsCache(); 72 | }; 73 | 74 | // Public API 75 | module.exports = { 76 | posts: posts, 77 | users: users, 78 | tags: tags, 79 | notifications: notifications, 80 | settings: settings, 81 | db: db, 82 | requestHandler: requestHandler, 83 | init: init 84 | }; 85 | -------------------------------------------------------------------------------- /core/test/integration/model/model_roles_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, it, before, beforeEach, afterEach */ 2 | var testUtils = require('../../utils'), 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 | testUtils.initData().then(function () { 23 | done(); 24 | }, done); 25 | }); 26 | 27 | afterEach(function (done) { 28 | testUtils.clearData().then(function () { 29 | done(); 30 | }, done); 31 | }); 32 | 33 | it("can browse roles", function (done) { 34 | RoleModel.browse().then(function (foundRoles) { 35 | should.exist(foundRoles); 36 | 37 | foundRoles.models.length.should.be.above(0); 38 | 39 | done(); 40 | }).then(null, done); 41 | }); 42 | 43 | it("can read roles", function (done) { 44 | RoleModel.read({id: 1}).then(function (foundRole) { 45 | should.exist(foundRole); 46 | 47 | done(); 48 | }).then(null, done); 49 | }); 50 | 51 | it("can edit roles", function (done) { 52 | RoleModel.read({id: 1}).then(function (foundRole) { 53 | should.exist(foundRole); 54 | 55 | return foundRole.set({name: "updated"}).save(); 56 | }).then(function () { 57 | return RoleModel.read({id: 1}); 58 | }).then(function (updatedRole) { 59 | should.exist(updatedRole); 60 | 61 | updatedRole.get("name").should.equal("updated"); 62 | 63 | done(); 64 | }).then(null, done); 65 | }); 66 | 67 | it("can add roles", function (done) { 68 | var newRole = { 69 | name: "test1", 70 | description: "test1 description" 71 | }; 72 | 73 | RoleModel.add(newRole).then(function (createdRole) { 74 | should.exist(createdRole); 75 | 76 | createdRole.attributes.name.should.equal(newRole.name); 77 | createdRole.attributes.description.should.equal(newRole.description); 78 | 79 | done(); 80 | }).then(null, done); 81 | }); 82 | 83 | it("can delete roles", function (done) { 84 | RoleModel.read({id: 1}).then(function (foundRole) { 85 | should.exist(foundRole); 86 | 87 | return RoleModel['delete'](1); 88 | }).then(function () { 89 | return RoleModel.browse(); 90 | }).then(function (foundRoles) { 91 | var hasRemovedId = foundRoles.any(function (role) { 92 | return role.id === 1; 93 | }); 94 | 95 | hasRemovedId.should.equal(false); 96 | 97 | done(); 98 | }).then(null, done); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /core/test/utils/index.js: -------------------------------------------------------------------------------- 1 | var knex = require('../../server/models/base').knex, 2 | when = require('when'), 3 | nodefn = require('when/node/function'), 4 | fs = require('fs-extra'), 5 | path = require('path'), 6 | migration = require("../../server/data/migration/"), 7 | Settings = require('../../server/models/settings').Settings, 8 | DataGenerator = require('./fixtures/data-generator'), 9 | API = require('./api'); 10 | 11 | function initData() { 12 | return migration.init(); 13 | } 14 | 15 | function clearData() { 16 | // we must always try to delete all tables 17 | return migration.reset(); 18 | } 19 | 20 | function insertPosts() { 21 | return when(knex('posts').insert(DataGenerator.forKnex.posts).then(function () { 22 | return knex('tags').insert(DataGenerator.forKnex.tags).then(function () { 23 | return knex('posts_tags').insert(DataGenerator.forKnex.posts_tags); 24 | }); 25 | })); 26 | } 27 | 28 | function insertMorePosts(max) { 29 | var lang, 30 | status, 31 | posts, 32 | promises = [], 33 | i, j, k = 0; 34 | 35 | max = max || 50; 36 | 37 | for (i = 0; i < 2; i += 1) { 38 | posts = []; 39 | lang = i % 2 ? 'en' : 'fr'; 40 | posts.push(DataGenerator.forKnex.createGenericPost(k++, null, lang)); 41 | 42 | for (j = 0; j < max; j += 1) { 43 | status = j % 2 ? 'draft' : 'published'; 44 | posts.push(DataGenerator.forKnex.createGenericPost(k++, status, lang)); 45 | } 46 | 47 | promises.push(knex('posts').insert(posts)); 48 | } 49 | 50 | return when.all(promises); 51 | } 52 | 53 | function insertDefaultUser() { 54 | var users = [], 55 | userRoles = []; 56 | 57 | users.push(DataGenerator.forKnex.createUser(DataGenerator.Content.users[0])); 58 | userRoles.push(DataGenerator.forKnex.createUserRole(1, 1)); 59 | return when(knex('users').insert(users).then(function () { 60 | return knex('roles_users').insert(userRoles); 61 | })); 62 | } 63 | 64 | function insertDefaultFixtures() { 65 | return when(insertDefaultUser().then(function () { 66 | return insertPosts(); 67 | })); 68 | } 69 | 70 | function loadExportFixture(filename) { 71 | var filepath = path.resolve(__dirname + '/fixtures/' + filename + '.json'); 72 | 73 | return nodefn.call(fs.readFile, filepath).then(function (fileContents) { 74 | var data; 75 | 76 | // Parse the json data 77 | try { 78 | data = JSON.parse(fileContents); 79 | } catch (e) { 80 | return when.reject(new Error("Failed to parse the file")); 81 | } 82 | 83 | return data; 84 | }); 85 | } 86 | 87 | module.exports = { 88 | initData: initData, 89 | clearData: clearData, 90 | insertDefaultFixtures: insertDefaultFixtures, 91 | insertPosts: insertPosts, 92 | insertMorePosts: insertMorePosts, 93 | insertDefaultUser: insertDefaultUser, 94 | 95 | loadExportFixture: loadExportFixture, 96 | 97 | DataGenerator: DataGenerator, 98 | API: API 99 | }; 100 | -------------------------------------------------------------------------------- /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('../utils'), 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.an.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 allow 4 underscores", function () { 59 | var processedMarkup = 60 | ghostdown().reduce(function (prev, processor) { 61 | return processor.filter(prev); 62 | }, "Ghost ____"); 63 | 64 | processedMarkup.should.match(/Ghost\s(?:_){4}$/); 65 | }); 66 | 67 | it("should correctly include an image", function () { 68 | [ 69 | "![image and another,/ image](http://dsurl.stuff)", 70 | "![](http://dsurl.stuff)" 71 | /* No ref-style for now 72 | "![image and another,/ image][test]\n\n[test]: http://dsurl.stuff", 73 | "![][test]\n\n[test]: http://dsurl.stuff" 74 | */ 75 | ] 76 | .forEach(function (imageMarkup) { 77 | var processedMarkup = 78 | ghostdown().reduce(function (prev, processor) { 79 | return processor.filter(prev); 80 | }, imageMarkup); 81 | 82 | processedMarkup.should.match(/]+|\([^\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/integration/model/model_permissions_spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, it, before, beforeEach, afterEach */ 2 | var testUtils = require('../../utils'), 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 | testUtils.initData().then(function () { 23 | done(); 24 | }, done); 25 | }); 26 | 27 | afterEach(function (done) { 28 | testUtils.clearData().then(function () { 29 | done(); 30 | }, done); 31 | }); 32 | 33 | it("can browse permissions", function (done) { 34 | PermissionModel.browse().then(function (foundPermissions) { 35 | should.exist(foundPermissions); 36 | 37 | foundPermissions.models.length.should.be.above(0); 38 | 39 | done(); 40 | }).then(null, done); 41 | }); 42 | 43 | it("can read permissions", function (done) { 44 | PermissionModel.read({id: 1}).then(function (foundPermission) { 45 | should.exist(foundPermission); 46 | 47 | done(); 48 | }).then(null, done); 49 | }); 50 | 51 | it("can edit permissions", function (done) { 52 | PermissionModel.read({id: 1}).then(function (foundPermission) { 53 | should.exist(foundPermission); 54 | 55 | return foundPermission.set({name: "updated"}).save(); 56 | }).then(function () { 57 | return PermissionModel.read({id: 1}); 58 | }).then(function (updatedPermission) { 59 | should.exist(updatedPermission); 60 | 61 | updatedPermission.get("name").should.equal("updated"); 62 | 63 | done(); 64 | }).then(null, done); 65 | }); 66 | 67 | it("can add permissions", function (done) { 68 | var newPerm = { 69 | name: "testperm1", 70 | object_type: 'test', 71 | action_type: 'test' 72 | }; 73 | 74 | PermissionModel.add(newPerm).then(function (createdPerm) { 75 | should.exist(createdPerm); 76 | 77 | createdPerm.attributes.name.should.equal(newPerm.name); 78 | 79 | done(); 80 | }).then(null, done); 81 | }); 82 | 83 | it("can delete permissions", function (done) { 84 | PermissionModel.read({id: 1}).then(function (foundPermission) { 85 | should.exist(foundPermission); 86 | 87 | return PermissionModel['delete'](1); 88 | }).then(function () { 89 | return PermissionModel.browse(); 90 | }).then(function (foundPermissions) { 91 | var hasRemovedId = foundPermissions.any(function (permission) { 92 | return permission.id === 1; 93 | }); 94 | 95 | hasRemovedId.should.equal(false); 96 | 97 | done(); 98 | }).then(null, done); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /core/client/tpl/settings/general.hbs: -------------------------------------------------------------------------------- 1 |
    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 | 60 | 61 |

    Include the date in your post URLs

    62 |
    63 | 64 |
    65 | 66 | 71 |

    Select a theme for your blog

    72 |
    73 | 74 |
    75 |
    76 |
    77 | -------------------------------------------------------------------------------- /core/test/functional/frontend/home_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests the homepage 3 | */ 4 | 5 | /*globals CasperTest, casper, __utils__, url, testPost, falseUser, email */ 6 | CasperTest.begin('Home page loads', 3, function suite(test) { 7 | casper.start(url, function then(response) { 8 | test.assertTitle('Ghost', 'The homepage should have a title and it should be Ghost'); 9 | test.assertExists('.content .post', 'There is at least one post on this page'); 10 | test.assertSelectorHasText('.poweredby', 'Proudly published with Ghost'); 11 | }); 12 | }, true); 13 | 14 | CasperTest.begin('Test helpers on homepage', 3, function suite(test) { 15 | casper.start(url, function then(response) { 16 | // body class 17 | test.assertExists('body.home-template', 'body_class outputs correct home-template class'); 18 | // post class 19 | test.assertExists('article.post', 'post_class outputs correct post class'); 20 | test.assertExists('article.tag-getting-started', 'post_class outputs correct tag class'); 21 | }); 22 | }, true); 23 | 24 | CasperTest.begin('Test navigating to Post', 4, function suite(test) { 25 | casper.thenOpen(url, function then(response) { 26 | var lastPost = '.content article:last-of-type', 27 | lastPostLink = lastPost + ' .post-title a'; 28 | 29 | test.assertExists(lastPost, 'there is a last child on the page'); 30 | test.assertSelectorHasText(lastPostLink, 'Welcome to Ghost', 'Is correct post'); 31 | 32 | casper.then(function testLink() { 33 | var link = this.evaluate(function (lastPostLink) { 34 | return document.querySelector(lastPostLink).getAttribute('href'); 35 | }, lastPostLink); 36 | 37 | test.assert(link === '/welcome-to-ghost/', 'Has correct link'); 38 | }); 39 | 40 | casper.thenClick(lastPostLink); 41 | 42 | casper.waitForResource(/welcome-to-ghost/).then(function (resource) { 43 | test.assert(resource.status === 200, 'resource got 200'); 44 | }); 45 | }); 46 | }, true); 47 | 48 | CasperTest.begin('Test navigating to Post with date permalink', 4, function suite(test) { 49 | CasperTest.Routines.togglePermalinks.run('on'); 50 | casper.thenOpen(url, function then(response) { 51 | var lastPost = '.content article:last-of-type', 52 | lastPostLink = lastPost + ' .post-title a', 53 | today = new Date(), 54 | dd = ("0" + today.getDate()).slice(-2), 55 | mm = ("0" + (today.getMonth() + 1)).slice(-2), 56 | yyyy = today.getFullYear(), 57 | postLink = '/' + yyyy + '/' + mm + '/' + dd + '/welcome-to-ghost/'; 58 | 59 | test.assertExists(lastPost, 'there is a last child on the page'); 60 | test.assertSelectorHasText(lastPostLink, 'Welcome to Ghost', 'Is correct post'); 61 | 62 | casper.then(function testLink() { 63 | var link = this.evaluate(function (lastPostLink) { 64 | return document.querySelector(lastPostLink).getAttribute('href'); 65 | }, lastPostLink); 66 | 67 | test.assert(link === postLink, 'Has correct link'); 68 | }); 69 | 70 | casper.thenClick(lastPostLink); 71 | 72 | casper.waitForResource(postLink).then(function (resource) { 73 | test.assert(resource.status === 200, 'resource got 200'); 74 | }); 75 | }); 76 | CasperTest.Routines.togglePermalinks.run('off'); 77 | }, false); 78 | 79 | -------------------------------------------------------------------------------- /core/server/bookshelf-session.js: -------------------------------------------------------------------------------- 1 | var Store = require('express').session.Store, 2 | time12h = 12 * 60 * 60 * 1000, 3 | BSStore, 4 | dataProvider, 5 | db, 6 | client; 7 | 8 | // Initialize store and clean old sessions 9 | BSStore = function BSStore(dataProvider, options) { 10 | var self = this; 11 | this.dataProvider = dataProvider; 12 | options = options || {}; 13 | Store.call(this, options); 14 | 15 | this.dataProvider.Session.findAll() 16 | .then(function (model) { 17 | var i, 18 | now = new Date().getTime(); 19 | for (i = 0; i < model.length; i = i + 1) { 20 | if (now > model.at(i).get('expires')) { 21 | self.destroy(model.at(i).get('id')); 22 | } 23 | } 24 | }); 25 | }; 26 | 27 | BSStore.prototype = new Store(); 28 | 29 | // store a given session 30 | BSStore.prototype.set = function (sid, sessData, callback) { 31 | var maxAge = sessData.cookie.maxAge, 32 | now = new Date().getTime(), 33 | expires = maxAge ? now + maxAge : now + time12h, 34 | sessionModel = this.dataProvider.Session; 35 | 36 | sessData = JSON.stringify(sessData); 37 | 38 | //necessary since bookshelf updates models if id is set 39 | sessionModel.forge({id: sid}).fetch() 40 | .then(function (model) { 41 | if (model) { 42 | sessionModel.forge({id: sid, expires: expires, sess: sessData }).save(); 43 | } else { 44 | sessionModel.forge({id: sid, expires: expires, sess: sessData }) 45 | .save(null, {method: 'insert'}); 46 | } 47 | callback(); 48 | }); 49 | }; 50 | 51 | // fetch a session, if session is expired delete it 52 | BSStore.prototype.get = function (sid, callback) { 53 | var now = new Date().getTime(), 54 | self = this, 55 | sess, 56 | expires; 57 | 58 | this.dataProvider.Session.forge({id: sid}) 59 | .fetch() 60 | .then(function (model) { 61 | if (model) { 62 | sess = JSON.parse(model.get('sess')); 63 | expires = model.get('expires'); 64 | if (now < expires) { 65 | callback(null, sess); 66 | } else { 67 | self.destroy(sid, callback); 68 | } 69 | } else { 70 | callback(); 71 | } 72 | }); 73 | }; 74 | 75 | // delete a given sessions 76 | BSStore.prototype.destroy = function (sid, callback) { 77 | this.dataProvider.Session.forge({id: sid}) 78 | .destroy() 79 | .then(function () { 80 | // check if callback is null 81 | // session.regenerate doesn't provide callback 82 | // cleanup at startup does neither 83 | if (callback) { 84 | callback(); 85 | } 86 | }); 87 | }; 88 | 89 | // get the count of all stored sessions 90 | BSStore.prototype.length = function (callback) { 91 | this.dataProvider.Session.findAll() 92 | .then(function (model) { 93 | callback(null, model.length); 94 | }); 95 | }; 96 | 97 | // delete all sessions 98 | BSStore.prototype.clear = function (callback) { 99 | this.dataProvider.Session.destroyAll() 100 | .then(function () { 101 | callback(); 102 | }); 103 | }; 104 | 105 | 106 | module.exports = BSStore; 107 | -------------------------------------------------------------------------------- /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/client/tpl/settings/user-profile.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 |

    Your Profile

    4 |
    5 | 6 |
    7 |
    8 | 9 |
    10 | 11 | 16 | 17 | 88 |
    89 | -------------------------------------------------------------------------------- /core/client/assets/sass/modules/breakpoint/parsers/_query.scss: -------------------------------------------------------------------------------- 1 | @function breakpoint-parse-query($query) { 2 | // Parse features out of an individual query 3 | $feature-holder: (); 4 | $query-holder: (); 5 | $length: length($query); 6 | 7 | @if $length == 2 { 8 | // If we've got a string/number, number/string, check to see if it's a valid string/number pair or two singles 9 | @if (type-of(nth($query, 1)) == 'string' and type-of(nth($query, 2)) == 'number') or (type-of(nth($query, 1)) == 'number' and type-of(nth($query, 2)) == 'string') { 10 | 11 | $number: ''; 12 | $value: ''; 13 | 14 | @if type-of(nth($query, 1)) == 'string' { 15 | $number: nth($query, 2); 16 | $value: nth($query, 1); 17 | } 18 | @else { 19 | $number: nth($query, 1); 20 | $value: nth($query, 2); 21 | } 22 | 23 | // If the string value can be a single value, check to see if the number passed in is a valid input for said single value. Fortunately, all current single-value options only accept unitless numbers, so this check is easy. 24 | @if breakpoint-single-string($value) { 25 | @if unitless($number) { 26 | $feature-holder: append($value, $number, space); 27 | $query-holder: append($query-holder, $feature-holder, comma); 28 | @return $query-holder; 29 | } 30 | } 31 | // If the string is a media type, split the query 32 | @if breakpoint-is-media($value) { 33 | $query-holder: append($query-holder, nth($query, 1)); 34 | $query-holder: append($query-holder, nth($query, 2)); 35 | @return $query-holder; 36 | } 37 | // If it's not a single feature, we're just going to assume it's a proper string/value pair, and roll with it. 38 | @else { 39 | $feature-holder: append($value, $number, space); 40 | $query-holder: append($query-holder, $feature-holder, comma); 41 | @return $query-holder; 42 | } 43 | 44 | } 45 | // If they're both numbers, we assume it's a double and roll with that 46 | @else if (type-of(nth($query, 1)) == 'number' and type-of(nth($query, 2)) == 'number') { 47 | $feature-holder: append(nth($query, 1), nth($query, 2), space); 48 | $query-holder: append($query-holder, $feature-holder, comma); 49 | @return $query-holder; 50 | } 51 | // If they're both strings and neither are singles, we roll with that. 52 | @else if (type-of(nth($query, 1)) == 'string' and type-of(nth($query, 2)) == 'string') { 53 | @if not breakpoint-single-string(nth($query, 1)) and not breakpoint-single-string(nth($query, 2)) { 54 | $feature-holder: append(nth($query, 1), nth($query, 2), space); 55 | $query-holder: append($query-holder, $feature-holder, comma); 56 | @return $query-holder; 57 | } 58 | } 59 | } 60 | @else if $length == 3 { 61 | // If we've got three items and none is a list, we check to see 62 | @if type-of(nth($query, 1)) != 'list' and type-of(nth($query, 2)) != 'list' and type-of(nth($query, 3)) != 'list' { 63 | // If none of the items are single string values and none of the values are media values, we're good. 64 | @if (not breakpoint-single-string(nth($query, 1)) and not breakpoint-single-string(nth($query, 2)) and not breakpoint-single-string(nth($query, 3))) and ((not breakpoint-is-media(nth($query, 1)) and not breakpoint-is-media(nth($query, 2)) and not breakpoint-is-media(nth($query, 3)))) { 65 | $feature-holder: append(nth($query, 1), nth($query, 2), space); 66 | $feature-holder: append($feature-holder, nth($query, 3), space); 67 | $query-holder: append($query-holder, $feature-holder, comma); 68 | @return $query-holder; 69 | } 70 | } 71 | } 72 | 73 | // If it's a single item, or if it's not a special case double or tripple, we can simply return the query. 74 | @return $query; 75 | } 76 | -------------------------------------------------------------------------------- /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/client/assets/sass/modules/breakpoint/_parsers.scss: -------------------------------------------------------------------------------- 1 | ////////////////////////////// 2 | // Import Parser Pieces 3 | ////////////////////////////// 4 | @import "parsers/query"; 5 | @import "parsers/single"; 6 | @import "parsers/double"; 7 | @import "parsers/triple"; 8 | @import "parsers/resolution"; 9 | 10 | ////////////////////////////// 11 | // General Breakpoint Parser 12 | ////////////////////////////// 13 | @function breakpoint-parse($query) { 14 | $private-breakpoint-context-placeholder: $private-breakpoint-context-placeholder + 1; 15 | 16 | // Set up Media Type 17 | $query-print: ''; 18 | 19 | $force-all: (($breakpoint-force-media-all == true) and ($breakpoint-default-media == 'all')); 20 | $empty-media: true; 21 | @if ($force-all == true) or ($breakpoint-default-media != 'all') { 22 | // Force the print of the default media type if (force all is true and default media type is all) or (default media type is not all) 23 | $query-print: $breakpoint-default-media; 24 | $empty-media: false; 25 | } 26 | 27 | 28 | $query-resolution: false; 29 | 30 | $query-holder: breakpoint-parse-query($query); 31 | 32 | 33 | 34 | // Loop over each parsed out query and write it to $query-print 35 | $first: true; 36 | 37 | @each $feature in $query-holder { 38 | $length: length($feature); 39 | 40 | // Parse a single feature 41 | @if ($length == 1) { 42 | // Feature is currenty a list, grab the actual value 43 | $feature: nth($feature, 1); 44 | 45 | // Media Type must by convention be the first item, so it's safe to flat override $query-print, which right now should only be the default media type 46 | @if (breakpoint-is-media($feature)) { 47 | @if ($force-all == true) or ($feature != 'all') { 48 | // Force the print of the default media type if (force all is true and default media type is all) or (default media type is not all) 49 | $query-print: $feature; 50 | $empty-media: false; 51 | 52 | // Set Context 53 | $context-setter: private-breakpoint-set-context(media, $query-print); 54 | } 55 | } 56 | @else { 57 | $parsed: breakpoint-parse-single($feature, $empty-media, $first); 58 | $query-print: '#{$query-print} #{$parsed}'; 59 | $first: false; 60 | } 61 | } 62 | // Parse a double feature 63 | @else if ($length == 2) { 64 | @if (breakpoint-is-resolution($feature) != false) { 65 | $query-resolution: $feature; 66 | } 67 | @else { 68 | $parsed: null; 69 | // If it's a string/number pair, 70 | // we check to see if one is a single-string value, 71 | // then we parse it as a normal double 72 | $alpha: nth($feature, 1); 73 | $beta: nth($feature, 2); 74 | @if breakpoint-single-string($alpha) or breakpoint-single-string($beta) { 75 | $parsed: breakpoint-parse-single($alpha, $empty-media, $first); 76 | $query-print: '#{$query-print} #{$parsed}'; 77 | $first: false; 78 | $parsed: breakpoint-parse-single($beta, $empty-media, $first); 79 | $query-print: '#{$query-print} #{$parsed}'; 80 | } 81 | @else { 82 | $parsed: breakpoint-parse-double($feature, $empty-media, $first); 83 | $query-print: '#{$query-print} #{$parsed}'; 84 | $first: false; 85 | } 86 | } 87 | } 88 | // Parse a triple feature 89 | @else if ($length == 3) { 90 | $parsed: breakpoint-parse-triple($feature, $empty-media, $first); 91 | $query-print: '#{$query-print} #{$parsed}'; 92 | $first: false; 93 | } 94 | 95 | } 96 | 97 | @if ($query-resolution != false) { 98 | $query-print: breakpoint-build-resolution($query-print, $query-resolution, $empty-media, $first); 99 | } 100 | 101 | // @return 'all'; 102 | 103 | @return $query-print; 104 | } 105 | -------------------------------------------------------------------------------- /core/server/plugins/index.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('underscore'), 3 | when = require('when'), 4 | errors = require('../errorHandling'), 5 | api = require('../api'), 6 | loader = require('./loader'), 7 | // Holds the available plugins 8 | availablePlugins = {}; 9 | 10 | 11 | function getInstalledPlugins() { 12 | return api.settings.read('installedPlugins').then(function (installed) { 13 | installed.value = installed.value || '[]'; 14 | 15 | try { 16 | installed = JSON.parse(installed.value); 17 | } catch (e) { 18 | return when.reject(e); 19 | } 20 | 21 | return installed; 22 | }); 23 | } 24 | 25 | function saveInstalledPlugins(installedPlugins) { 26 | return getInstalledPlugins().then(function (currentInstalledPlugins) { 27 | var updatedPluginsInstalled = _.uniq(installedPlugins.concat(currentInstalledPlugins)); 28 | 29 | return api.settings.edit('installedPlugins', updatedPluginsInstalled); 30 | }); 31 | } 32 | 33 | module.exports = { 34 | init: function () { 35 | var pluginsToLoad; 36 | 37 | try { 38 | // We have to parse the value because it's a string 39 | api.settings.read('activePlugins').then(function (aPlugins) { 40 | pluginsToLoad = JSON.parse(aPlugins.value) || []; 41 | }); 42 | } catch (e) { 43 | errors.logError( 44 | 'Failed to parse activePlugins setting value: ' + e.message, 45 | 'Your plugins will not be loaded.', 46 | 'Check your settings table for typos in the activePlugins value. It should look like: ["plugin-1", "plugin2"] (double quotes required).' 47 | ); 48 | return when.resolve(); 49 | } 50 | 51 | // Grab all installed plugins, install any not already installed that are in pluginsToLoad. 52 | return getInstalledPlugins().then(function (installedPlugins) { 53 | var loadedPlugins = {}, 54 | recordLoadedPlugin = function (name, loadedPlugin) { 55 | // After loading the plugin, add it to our hash of loaded plugins 56 | loadedPlugins[name] = loadedPlugin; 57 | 58 | return when.resolve(loadedPlugin); 59 | }, 60 | loadPromises = _.map(pluginsToLoad, function (plugin) { 61 | // If already installed, just activate the plugin 62 | if (_.contains(installedPlugins, plugin)) { 63 | return loader.activatePluginByName(plugin).then(function (loadedPlugin) { 64 | return recordLoadedPlugin(plugin, loadedPlugin); 65 | }); 66 | } 67 | 68 | // Install, then activate the plugin 69 | return loader.installPluginByName(plugin).then(function () { 70 | return loader.activatePluginByName(plugin); 71 | }).then(function (loadedPlugin) { 72 | return recordLoadedPlugin(plugin, loadedPlugin); 73 | }); 74 | }); 75 | 76 | return when.all(loadPromises).then(function () { 77 | // Save our installed plugins to settings 78 | return saveInstalledPlugins(_.keys(loadedPlugins)); 79 | }).then(function () { 80 | // Extend the loadedPlugins onto the available plugins 81 | _.extend(availablePlugins, loadedPlugins); 82 | }).otherwise(function (err) { 83 | errors.logError( 84 | err.message || err, 85 | 'The plugin will not be loaded', 86 | 'Check with the plugin creator, or read the plugin documentation for more details on plugin requirements' 87 | ); 88 | }); 89 | }); 90 | }, 91 | availablePlugins: availablePlugins 92 | }; -------------------------------------------------------------------------------- /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.waitForSelector('.usermenu-signout a'); 24 | casper.thenClick('.usermenu-signout a'); 25 | casper.waitForResource(/ghost\/signin/); 26 | 27 | casper.waitForSelector('.notification-success', function onSuccess() { 28 | test.assert(true, 'Got success notification'); 29 | }, function onTimeout() { 30 | test.assert(false, 'No success notification :('); 31 | }); 32 | }, true); 33 | 34 | // has to be done after signing out 35 | CasperTest.begin("Can't spam signin", 3, function suite(test) { 36 | casper.thenOpen(url + "ghost/signin/", function testTitle() { 37 | test.assertTitle("Ghost Admin", "Ghost admin has no title"); 38 | }); 39 | 40 | casper.waitFor(function checkOpaque() { 41 | return this.evaluate(function () { 42 | var loginBox = document.querySelector('.login-box'); 43 | return window.getComputedStyle(loginBox).getPropertyValue('display') === "table" 44 | && window.getComputedStyle(loginBox).getPropertyValue('opacity') === "1"; 45 | }); 46 | }, function then() { 47 | this.fill("#login", falseUser, true); 48 | casper.wait(200, function doneWait() { 49 | this.fill("#login", falseUser, true); 50 | }); 51 | 52 | }); 53 | 54 | casper.waitForSelector('.notification-error', function onSuccess() { 55 | test.assert(true, 'Got error notification'); 56 | test.assertSelectorDoesntHaveText('.notification-error', '[object Object]'); 57 | }, function onTimeout() { 58 | test.assert(false, 'No error notification :('); 59 | }); 60 | }, true); 61 | 62 | CasperTest.begin("Ghost signup fails properly", 5, function suite(test) { 63 | casper.thenOpen(url + "ghost/signup/", function then() { 64 | test.assertEquals(casper.getCurrentUrl(), url + "ghost/signup/", "Reached signup page"); 65 | }); 66 | 67 | casper.then(function signupWithShortPassword() { 68 | this.fill("#signup", {email: email, password: 'test'}, true); 69 | }); 70 | 71 | // should now throw a short password error 72 | casper.waitForResource(/signup/); 73 | casper.waitForSelector('.notification-error', function onSuccess() { 74 | test.assert(true, 'Got error notification'); 75 | test.assertSelectorDoesntHaveText('.notification-error', '[object Object]'); 76 | }, function onTimeout() { 77 | test.assert(false, 'No error notification :('); 78 | }); 79 | 80 | casper.then(function signupWithLongPassword() { 81 | this.fill("#signup", {email: email, password: 'testing1234'}, true); 82 | }); 83 | 84 | // should now throw a 1 user only error 85 | casper.waitForResource(/signup/); 86 | casper.waitForSelector('.notification-error', function onSuccess() { 87 | test.assert(true, 'Got error notification'); 88 | test.assertSelectorDoesntHaveText('.notification-error', '[object Object]'); 89 | }, function onTimeout() { 90 | test.assert(false, 'No error notification :('); 91 | }); 92 | }, true); -------------------------------------------------------------------------------- /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/server/api/users.js: -------------------------------------------------------------------------------- 1 | var when = require('when'), 2 | _ = require('underscore'), 3 | dataProvider = require('../models'), 4 | settings = require('./settings'), 5 | ONE_DAY = 86400000, 6 | filteredAttributes = ['password', 'created_by', 'updated_by', 'last_login'], 7 | users; 8 | 9 | // ## Users 10 | users = { 11 | // #### Browse 12 | 13 | // **takes:** options object 14 | browse: function browse(options) { 15 | // **returns:** a promise for a collection of users in a json object 16 | 17 | return dataProvider.User.browse(options).then(function (result) { 18 | var i = 0, 19 | omitted = {}; 20 | 21 | if (result) { 22 | omitted = result.toJSON(); 23 | } 24 | 25 | for (i = 0; i < omitted.length; i = i + 1) { 26 | omitted[i] = _.omit(omitted[i], filteredAttributes); 27 | } 28 | 29 | return omitted; 30 | }); 31 | }, 32 | 33 | // #### Read 34 | 35 | // **takes:** an identifier (id or slug?) 36 | read: function read(args) { 37 | // **returns:** a promise for a single user in a json object 38 | if (args.id === 'me') { 39 | args = {id: this.user}; 40 | } 41 | 42 | return dataProvider.User.read(args).then(function (result) { 43 | if (result) { 44 | var omitted = _.omit(result.toJSON(), filteredAttributes); 45 | return omitted; 46 | } 47 | 48 | return when.reject({errorCode: 404, message: 'User not found'}); 49 | }); 50 | }, 51 | 52 | // #### Edit 53 | 54 | // **takes:** a json object representing a user 55 | edit: function edit(userData) { 56 | // **returns:** a promise for the resulting user in a json object 57 | userData.id = this.user; 58 | return dataProvider.User.edit(userData).then(function (result) { 59 | if (result) { 60 | var omitted = _.omit(result.toJSON(), filteredAttributes); 61 | return omitted; 62 | } 63 | return when.reject({errorCode: 404, message: 'User not found'}); 64 | }); 65 | }, 66 | 67 | // #### Add 68 | 69 | // **takes:** a json object representing a user 70 | add: function add(userData) { 71 | 72 | // **returns:** a promise for the resulting user in a json object 73 | return dataProvider.User.add(userData); 74 | }, 75 | 76 | // #### Check 77 | // Checks a password matches the given email address 78 | 79 | // **takes:** a json object representing a user 80 | check: function check(userData) { 81 | // **returns:** on success, returns a promise for the resulting user in a json object 82 | return dataProvider.User.check(userData); 83 | }, 84 | 85 | // #### Change Password 86 | 87 | // **takes:** a json object representing a user 88 | changePassword: function changePassword(userData) { 89 | // **returns:** on success, returns a promise for the resulting user in a json object 90 | return dataProvider.User.changePassword(userData); 91 | }, 92 | 93 | generateResetToken: function generateResetToken(email) { 94 | // TODO: Do we want to be able to pass this in? 95 | var expires = Date.now() + ONE_DAY; 96 | return settings.read('dbHash').then(function (dbHash) { 97 | return dataProvider.User.generateResetToken(email, expires, dbHash); 98 | }); 99 | }, 100 | 101 | validateToken: function validateToken(token) { 102 | return settings.read('dbHash').then(function (dbHash) { 103 | return dataProvider.User.validateToken(token, dbHash); 104 | }); 105 | }, 106 | 107 | resetPassword: function resetPassword(token, newPassword, ne2Password) { 108 | return settings.read('dbHash').then(function (dbHash) { 109 | return dataProvider.User.resetPassword(token, newPassword, ne2Password, dbHash); 110 | }); 111 | } 112 | }; 113 | 114 | module.exports = users; 115 | module.exports.filteredAttributes = filteredAttributes; -------------------------------------------------------------------------------- /core/client/assets/sass/modules/breakpoint.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Breakpoint Sass 2.0.6 3 | * Last updated: July 2013 4 | * Copyright: Mason Wendell 2012 - MIT Licensed 5 | * Source: https://github.com/canarymason/breakpoint 6 | */ 7 | 8 | ////////////////////////////// 9 | // Default Variables 10 | ////////////////////////////// 11 | // Default Features 12 | $breakpoint-default-media: all !default; 13 | $breakpoint-default-feature: min-width !default; 14 | $breakpoint-default-pair: width !default; 15 | 16 | // Default Transforms 17 | $breakpoint-force-media-all: false !default; 18 | $breakpoint-to-ems: false !default; 19 | $breakpoint-resolutions: true !default; 20 | 21 | // Default No Query Options 22 | $breakpoint-no-queries: false !default; 23 | $breakpoint-no-query-fallbacks: false !default; 24 | 25 | // Deftault Base Font Size 26 | $breakpoint-base-font-size: 16px !default; 27 | 28 | // Legacy Syntax Support 29 | $breakpoint-legacy-syntax: false !default; 30 | 31 | ////////////////////////////// 32 | // Imports 33 | ////////////////////////////// 34 | @import 'breakpoint/context'; 35 | @import 'breakpoint/helpers'; 36 | @import 'breakpoint/parsers'; 37 | @import 'breakpoint/no-query'; 38 | 39 | @import 'breakpoint/respond-to'; 40 | 41 | ////////////////////////////// 42 | // Breakpoint Mixin 43 | ////////////////////////////// 44 | 45 | @mixin breakpoint($query, $no-query: false) { 46 | // Internal Variables 47 | $query-string: ''; 48 | 49 | // Reset contexts 50 | @include private-breakpoint-reset-contexts(); 51 | 52 | // Test to see if it's a comma-separated list 53 | $or-list: is-breakpoint-list($query); 54 | $query-fallback: false; 55 | 56 | 57 | @if ($or-list != false and $breakpoint-legacy-syntax == false) { 58 | $length: length($query); 59 | 60 | $last: nth($query, $length); 61 | $query-fallback: breakpoint-no-query($last); 62 | 63 | @if ($query-fallback != false) { 64 | $length: $length - 1; 65 | } 66 | 67 | 68 | @for $i from 1 through $length { 69 | @if $i == 1 { 70 | $query-string: breakpoint-parse(nth($query, $i)); 71 | } 72 | @else { 73 | $query-string: $query-string + ', ' + breakpoint-parse(nth($query, $i)); 74 | } 75 | } 76 | } 77 | @else { 78 | @if ($breakpoint-legacy-syntax == true) { 79 | $length: length($query); 80 | 81 | $last: nth($query, $length); 82 | $query-fallback: breakpoint-no-query($last); 83 | 84 | @if ($query-fallback != false) { 85 | $length: $length - 1; 86 | } 87 | 88 | $mq: (); 89 | 90 | @for $i from 1 through $length { 91 | $mq: append($mq, nth($query, $i), comma); 92 | } 93 | 94 | $query-string: breakpoint-parse($mq); 95 | } 96 | @else { 97 | $query-string: breakpoint-parse($query); 98 | } 99 | } 100 | 101 | // Allow for an as-needed override or usage of no query fallback. 102 | @if $no-query != false { 103 | $query-fallback: $no-query; 104 | } 105 | 106 | 107 | // Print Out Query String 108 | @if not $breakpoint-no-queries { 109 | @media #{$query-string} { 110 | @content; 111 | } 112 | } 113 | 114 | @if $breakpoint-no-query-fallbacks != false { 115 | 116 | $type: type-of($breakpoint-no-query-fallbacks); 117 | $print: false; 118 | 119 | @if ($type == 'bool') { 120 | $print: true; 121 | } 122 | @else if ($type == 'string') { 123 | @if $query-fallback == $breakpoint-no-query-fallbacks { 124 | $print: true; 125 | } 126 | } 127 | @else if ($type == 'list') { 128 | @each $wrapper in $breakpoint-no-query-fallbacks { 129 | @if $query-fallback == $wrapper { 130 | $print: true; 131 | } 132 | } 133 | } 134 | 135 | // Write Fallback 136 | @if ($query-fallback != false) and ($print == true) { 137 | $type-fallback: type-of($query-fallback); 138 | 139 | @if ($type-fallback != 'bool') { 140 | #{$query-fallback} & { 141 | @content; 142 | } 143 | } 144 | @else { 145 | @content; 146 | } 147 | } 148 | } 149 | 150 | @include private-breakpoint-reset-contexts(); 151 | } 152 | -------------------------------------------------------------------------------- /core/client/assets/sass/modules/breakpoint/_context.scss: -------------------------------------------------------------------------------- 1 | ////////////////////////////// 2 | // Private Breakpoint Variables 3 | ////////////////////////////// 4 | $private-breakpoint-context-holder: (); 5 | $private-breakpoint-context-placeholder: 0; 6 | 7 | ////////////////////////////// 8 | // Breakpoint Has Context 9 | // Returns whether or not you are inside a Breakpoint query 10 | ////////////////////////////// 11 | @function breakpoint-has-context() { 12 | @if length($private-breakpoint-context-placeholder) { 13 | @return true; 14 | } 15 | @else { 16 | @return false; 17 | } 18 | } 19 | 20 | 21 | ////////////////////////////// 22 | // Breakpoint Get Context 23 | // $feature: Input feature to get it's current MQ context. Returns false if no context 24 | ////////////////////////////// 25 | @function breakpoint-get-context($feature) { 26 | @each $context in $private-breakpoint-context-holder { 27 | @if $feature == nth($context, 1) { 28 | // strip feature name 29 | $values: (); 30 | @for $i from 2 through length($context) { 31 | $values: append($values, nth($context, $i), comma); 32 | } 33 | 34 | $length: length($values) + 1; 35 | @for $i from $length through $private-breakpoint-context-placeholder { 36 | // Apply the Default Media type if feature is media 37 | @if $feature == 'media' { 38 | $values: append($values, $breakpoint-default-media, comma); 39 | } 40 | @else { 41 | $values: append($values, false, comma); 42 | } 43 | } 44 | 45 | @return $values; 46 | } 47 | } 48 | 49 | @return false; 50 | } 51 | 52 | ////////////////////////////// 53 | // Private function to set context 54 | ////////////////////////////// 55 | @function private-breakpoint-set-context($feature, $value) { 56 | @if $value == 'monochrome' { 57 | $feature: 'monochrome'; 58 | } 59 | 60 | $placeholder-plus-one: ($private-breakpoint-context-placeholder + 1); 61 | 62 | $holder: (); 63 | 64 | @if $private-breakpoint-context-placeholder == 1 { 65 | $holder: ($feature $value); 66 | $private-breakpoint-context-holder: append($private-breakpoint-context-holder, $holder, comma); 67 | @return true; 68 | 69 | } @else { 70 | $feature-used: false; 71 | @each $context in $private-breakpoint-context-holder { 72 | @if nth($context, 1) == $feature { 73 | $feature-used: $context; 74 | } 75 | } 76 | 77 | @if $feature-used != false { 78 | $holder: $feature; 79 | @for $i from 2 through $placeholder-plus-one { 80 | @if $i <= length($feature-used) { 81 | $holder: append($holder, nth($feature-used, $i), space); 82 | } @elseif $i < $placeholder-plus-one { 83 | $holder: append($holder, false, space); 84 | } @else { 85 | $holder: append($holder, $value, space); 86 | } 87 | } 88 | } 89 | @elseif $feature-used == false { 90 | $holder: $feature; 91 | @for $i from 2 through $placeholder-plus-one { 92 | @if $i < $placeholder-plus-one { 93 | // Apply the Default Media type if feature is media 94 | @if $feature == 'media' { 95 | $holder: append($holder, $breakpoint-default-media, space); 96 | } 97 | @else { 98 | $holder: append($holder, false, space); 99 | } 100 | 101 | } @else { 102 | $holder: append($holder, $value, space); 103 | } 104 | } 105 | } 106 | 107 | // Rebuild context 108 | $rebuild: (); 109 | @if $feature-used != false { 110 | @each $context in $private-breakpoint-context-holder { 111 | @if nth($context, 1) == nth($holder, 1) { 112 | $rebuild: append($rebuild, $holder, comma); 113 | } @else { 114 | $rebuild: append($rebuild, $context, comma); 115 | } 116 | } 117 | 118 | } @else { 119 | $rebuild: append($private-breakpoint-context-holder, $holder, comma); 120 | } 121 | $private-breakpoint-context-holder: $rebuild; 122 | } 123 | 124 | @return true; 125 | } 126 | 127 | ////////////////////////////// 128 | // Private function to reset context 129 | ////////////////////////////// 130 | @mixin private-breakpoint-reset-contexts { 131 | $private-breakpoint-context-holder: (); 132 | $private-breakpoint-context-placeholder: 0; 133 | } 134 | --------------------------------------------------------------------------------