├── .gitignore ├── core ├── shared │ ├── robots.txt │ ├── img │ │ ├── user-cover.png │ │ └── user-image.png │ ├── favicon.ico │ └── lib │ │ └── showdown │ │ └── extensions │ │ └── ghostimagepreview.js ├── client │ └── assets │ │ ├── img │ │ ├── large.png │ │ ├── medium.png │ │ ├── small.png │ │ ├── 404-ghost.png │ │ ├── loadingcat.gif │ │ ├── 404-ghost@2x.png │ │ ├── contributors │ │ │ ├── wjake │ │ │ ├── ErisDS │ │ │ ├── halfdan │ │ │ ├── hswolff │ │ │ ├── jgable │ │ │ ├── rwjblue │ │ │ ├── sebgie │ │ │ ├── JohnONolan │ │ │ ├── SiR-DanieL │ │ │ ├── cobbspur │ │ │ ├── jaswilli │ │ │ ├── javorszky │ │ │ ├── jillesme │ │ │ ├── morficus │ │ │ ├── novaugust │ │ │ ├── shindakun │ │ │ ├── PaulAdamDavis │ │ │ ├── felixrieseberg │ │ │ ├── manuelmitasch │ │ │ └── mattiascibien │ │ ├── touch-icon-ipad.png │ │ └── touch-icon-iphone.png │ │ ├── fonts │ │ ├── icons.eot │ │ ├── icons.ttf │ │ └── icons.woff │ │ └── lib │ │ └── touch-editor.js ├── server │ ├── data │ │ ├── import │ │ │ ├── 001.js │ │ │ ├── 002.js │ │ │ └── 003.js │ │ ├── utils │ │ │ └── clients │ │ │ │ ├── index.js │ │ │ │ ├── sqlite3.js │ │ │ │ ├── pg.js │ │ │ │ └── mysql.js │ │ ├── default-settings.json │ │ ├── export │ │ │ └── index.js │ │ ├── versioning │ │ │ └── index.js │ │ ├── migration │ │ │ └── commands.js │ │ └── fixtures │ │ │ └── permissions │ │ │ └── index.js │ ├── helpers │ │ ├── tpl │ │ │ ├── nav.hbs │ │ │ └── pagination.hbs │ │ ├── encode.js │ │ ├── title.js │ │ ├── utils.js │ │ ├── ghost_script_tags.js │ │ ├── is.js │ │ ├── url.js │ │ ├── date.js │ │ ├── ghost_foot.js │ │ ├── excerpt.js │ │ ├── asset.js │ │ ├── meta_description.js │ │ ├── post_class.js │ │ ├── plural.js │ │ ├── content.js │ │ ├── author.js │ │ ├── meta_title.js │ │ ├── page_url.js │ │ ├── tags.js │ │ ├── pagination.js │ │ ├── has.js │ │ ├── template.js │ │ ├── foreach.js │ │ └── body_class.js │ ├── routes │ │ ├── index.js │ │ ├── admin.js │ │ ├── frontend.js │ │ └── api.js │ ├── permissions │ │ ├── object-type-model-map.js │ │ └── effective.js │ ├── utils │ │ ├── sequence.js │ │ ├── pipeline.js │ │ └── index.js │ ├── models │ │ ├── client.js │ │ ├── accesstoken.js │ │ ├── refreshtoken.js │ │ ├── app-field.js │ │ ├── app-setting.js │ │ ├── permission.js │ │ ├── tag.js │ │ ├── app.js │ │ ├── index.js │ │ ├── basetoken.js │ │ └── role.js │ ├── errors │ │ ├── email-error.js │ │ ├── not-found-error.js │ │ ├── bad-request-error.js │ │ ├── unauthorized-error.js │ │ ├── no-permission-error.js │ │ ├── internal-server-error.js │ │ ├── unsupported-media-type-error.js │ │ ├── request-too-large-error.js │ │ ├── validation-error.js │ │ └── data-import-error.js │ ├── storage │ │ ├── index.js │ │ ├── base.js │ │ └── local-file-store.js │ ├── api │ │ ├── tags.js │ │ ├── utils.js │ │ ├── upload.js │ │ ├── slugs.js │ │ ├── configuration.js │ │ ├── roles.js │ │ └── themes.js │ ├── views │ │ ├── error.hbs │ │ ├── default.hbs │ │ └── user-error.hbs │ ├── apps │ │ ├── dependencies.js │ │ ├── permissions.js │ │ ├── sandbox.js │ │ ├── proxy.js │ │ └── index.js │ ├── controllers │ │ └── admin.js │ ├── middleware │ │ ├── ghost-busboy.js │ │ └── auth-strategies.js │ ├── xmlrpc.js │ ├── filters.js │ └── email-templates │ │ ├── reset-password.html │ │ └── test.html └── index.js ├── loaderio-cfd0e26d73d2c018ce430ea0e6b739e6.html ├── content ├── apps │ └── README.md ├── plugins │ └── README.md ├── themes │ ├── casper │ │ ├── package.json │ │ ├── assets │ │ │ ├── fonts │ │ │ │ ├── icons.eot │ │ │ │ ├── icons.ttf │ │ │ │ ├── icons.woff │ │ │ │ ├── casper-icons.eot │ │ │ │ ├── casper-icons.ttf │ │ │ │ ├── casper-icons.woff │ │ │ │ └── icons.svg │ │ │ └── js │ │ │ │ ├── ja.js │ │ │ │ ├── jquery.fitvids.js │ │ │ │ └── index.js │ │ ├── default_edits.hbs │ │ ├── index_edits.hbs │ │ ├── partials │ │ │ └── loop.hbs │ │ ├── tag.hbs │ │ ├── LICENSE │ │ ├── README.md │ │ ├── post_edits.hbs │ │ ├── page.hbs │ │ ├── author.hbs │ │ ├── index.hbs │ │ └── default.hbs │ └── kevgriffin │ │ ├── assets │ │ ├── fonts │ │ │ ├── icons.eot │ │ │ ├── icons.ttf │ │ │ └── icons.woff │ │ └── js │ │ │ ├── index.js │ │ │ └── jquery.fitvids.js │ │ ├── page.hbs │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.hbs │ │ ├── default.hbs │ │ └── post.hbs ├── data │ ├── ghost.db │ ├── ghost-dev.db │ └── README.md └── images │ ├── 2014 │ ├── Oct │ │ └── everleap-20-1--1-.png │ └── Feb │ │ ├── Kevin_in_Snow_with_Boys.jpeg │ │ └── Kevin_in_Snow_with_Boys-1.jpeg │ └── README.md ├── bower.json ├── npm-debug.log ├── index.js ├── LICENSE └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /core/shared/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /ghost/ -------------------------------------------------------------------------------- /loaderio-cfd0e26d73d2c018ce430ea0e6b739e6.html: -------------------------------------------------------------------------------- 1 | loaderio-cfd0e26d73d2c018ce430ea0e6b739e6 -------------------------------------------------------------------------------- /content/apps/README.md: -------------------------------------------------------------------------------- 1 | # Content / Apps 2 | 3 | Coming soon, Ghost apps will appear here. -------------------------------------------------------------------------------- /content/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Content / Plugins 2 | 3 | Coming soon, Ghost plugins will appear here. -------------------------------------------------------------------------------- /content/themes/casper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Casper", 3 | "version": "1.1.1" 4 | } 5 | -------------------------------------------------------------------------------- /content/data/ghost.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/data/ghost.db -------------------------------------------------------------------------------- /content/data/ghost-dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/data/ghost-dev.db -------------------------------------------------------------------------------- /core/shared/img/user-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/shared/img/user-cover.png -------------------------------------------------------------------------------- /core/shared/img/user-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/shared/img/user-image.png -------------------------------------------------------------------------------- /core/client/assets/img/large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/large.png -------------------------------------------------------------------------------- /core/client/assets/img/medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/medium.png -------------------------------------------------------------------------------- /core/client/assets/img/small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/small.png -------------------------------------------------------------------------------- /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/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/fonts/icons.eot -------------------------------------------------------------------------------- /core/client/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /core/client/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/fonts/icons.woff -------------------------------------------------------------------------------- /core/client/assets/img/404-ghost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/404-ghost.png -------------------------------------------------------------------------------- /core/client/assets/img/loadingcat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/loadingcat.gif -------------------------------------------------------------------------------- /core/client/assets/img/404-ghost@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/404-ghost@2x.png -------------------------------------------------------------------------------- /core/client/assets/img/contributors/wjake: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/wjake -------------------------------------------------------------------------------- /core/client/assets/img/contributors/ErisDS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/ErisDS -------------------------------------------------------------------------------- /core/client/assets/img/contributors/halfdan: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/halfdan -------------------------------------------------------------------------------- /core/client/assets/img/contributors/hswolff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/hswolff -------------------------------------------------------------------------------- /core/client/assets/img/contributors/jgable: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/jgable -------------------------------------------------------------------------------- /core/client/assets/img/contributors/rwjblue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/rwjblue -------------------------------------------------------------------------------- /core/client/assets/img/contributors/sebgie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/sebgie -------------------------------------------------------------------------------- /core/client/assets/img/touch-icon-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/touch-icon-ipad.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. -------------------------------------------------------------------------------- /content/images/2014/Oct/everleap-20-1--1-.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/images/2014/Oct/everleap-20-1--1-.png -------------------------------------------------------------------------------- /content/themes/casper/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/themes/casper/assets/fonts/icons.eot -------------------------------------------------------------------------------- /content/themes/casper/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/themes/casper/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /content/themes/casper/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/themes/casper/assets/fonts/icons.woff -------------------------------------------------------------------------------- /core/client/assets/img/contributors/JohnONolan: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/JohnONolan -------------------------------------------------------------------------------- /core/client/assets/img/contributors/SiR-DanieL: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/SiR-DanieL -------------------------------------------------------------------------------- /core/client/assets/img/contributors/cobbspur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/cobbspur -------------------------------------------------------------------------------- /core/client/assets/img/contributors/jaswilli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/jaswilli -------------------------------------------------------------------------------- /core/client/assets/img/contributors/javorszky: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/javorszky -------------------------------------------------------------------------------- /core/client/assets/img/contributors/jillesme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/jillesme -------------------------------------------------------------------------------- /core/client/assets/img/contributors/morficus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/morficus -------------------------------------------------------------------------------- /core/client/assets/img/contributors/novaugust: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/novaugust -------------------------------------------------------------------------------- /core/client/assets/img/contributors/shindakun: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/shindakun -------------------------------------------------------------------------------- /core/client/assets/img/touch-icon-iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/touch-icon-iphone.png -------------------------------------------------------------------------------- /content/themes/kevgriffin/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/themes/kevgriffin/assets/fonts/icons.eot -------------------------------------------------------------------------------- /content/themes/kevgriffin/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/themes/kevgriffin/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /content/themes/casper/assets/fonts/casper-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/themes/casper/assets/fonts/casper-icons.eot -------------------------------------------------------------------------------- /content/themes/casper/assets/fonts/casper-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/themes/casper/assets/fonts/casper-icons.ttf -------------------------------------------------------------------------------- /content/themes/kevgriffin/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/themes/kevgriffin/assets/fonts/icons.woff -------------------------------------------------------------------------------- /core/client/assets/img/contributors/PaulAdamDavis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/PaulAdamDavis -------------------------------------------------------------------------------- /core/client/assets/img/contributors/felixrieseberg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/felixrieseberg -------------------------------------------------------------------------------- /core/client/assets/img/contributors/manuelmitasch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/manuelmitasch -------------------------------------------------------------------------------- /core/client/assets/img/contributors/mattiascibien: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/core/client/assets/img/contributors/mattiascibien -------------------------------------------------------------------------------- /content/images/2014/Feb/Kevin_in_Snow_with_Boys.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/images/2014/Feb/Kevin_in_Snow_with_Boys.jpeg -------------------------------------------------------------------------------- /content/themes/casper/assets/fonts/casper-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/themes/casper/assets/fonts/casper-icons.woff -------------------------------------------------------------------------------- /content/images/2014/Feb/Kevin_in_Snow_with_Boys-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1kevgriff/kevgriffincom/master/content/images/2014/Feb/Kevin_in_Snow_with_Boys-1.jpeg -------------------------------------------------------------------------------- /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 | }; 9 | -------------------------------------------------------------------------------- /core/server/data/import/002.js: -------------------------------------------------------------------------------- 1 | var Importer000 = require('./000'); 2 | 3 | module.exports = { 4 | Importer002: Importer000, 5 | importData: function (data) { 6 | return new Importer000.importData(data); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /core/server/data/import/003.js: -------------------------------------------------------------------------------- 1 | var Importer000 = require('./000'); 2 | 3 | module.exports = { 4 | Importer003: Importer000, 5 | importData: function (data) { 6 | return new Importer000.importData(data); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /core/server/helpers/tpl/nav.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /content/themes/casper/default_edits.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /core/server/data/utils/clients/index.js: -------------------------------------------------------------------------------- 1 | var sqlite3 = require('./sqlite3'), 2 | mysql = require('./mysql'), 3 | pg = require('./pg'); 4 | 5 | module.exports = { 6 | sqlite3: sqlite3, 7 | mysql: mysql, 8 | pg: pg, 9 | postgres: pg, 10 | postgresql: pg 11 | }; 12 | -------------------------------------------------------------------------------- /core/server/routes/index.js: -------------------------------------------------------------------------------- 1 | var api = require('./api'), 2 | admin = require('./admin'), 3 | frontend = require('./frontend'); 4 | 5 | module.exports = { 6 | apiBaseUri: '/ghost/api/v0.1/', 7 | api: api, 8 | admin: admin, 9 | frontend: frontend 10 | }; 11 | -------------------------------------------------------------------------------- /content/themes/kevgriffin/assets/js/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main JS file for Casper behaviours 3 | */ 4 | 5 | /*globals jQuery, document */ 6 | (function ($) { 7 | "use strict"; 8 | 9 | $(document).ready(function(){ 10 | 11 | $(".post-content").fitVids(); 12 | 13 | }); 14 | 15 | }(jQuery)); -------------------------------------------------------------------------------- /core/server/permissions/object-type-model-map.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | post: require('../models/post').Post, 3 | role: require('../models/role').Role, 4 | user: require('../models/user').User, 5 | permission: require('../models/permission').Permission, 6 | setting: require('../models/settings').Settings 7 | }; 8 | -------------------------------------------------------------------------------- /core/server/routes/admin.js: -------------------------------------------------------------------------------- 1 | var admin = require('../controllers/admin'), 2 | express = require('express'), 3 | 4 | adminRoutes; 5 | 6 | adminRoutes = function () { 7 | var router = express.Router(); 8 | 9 | router.get('*', admin.index); 10 | 11 | return router; 12 | }; 13 | 14 | module.exports = adminRoutes; 15 | -------------------------------------------------------------------------------- /core/server/utils/sequence.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | 3 | function sequence(tasks) { 4 | return Promise.reduce(tasks, function (results, task) { 5 | return task().then(function (result) { 6 | results.push(result); 7 | 8 | return results; 9 | }); 10 | }, []); 11 | } 12 | 13 | module.exports = sequence; 14 | -------------------------------------------------------------------------------- /core/index.js: -------------------------------------------------------------------------------- 1 | // # Ghost bootloader 2 | // Orchestrates the loading of Ghost 3 | // When run from command line. 4 | 5 | var server = require('./server'); 6 | 7 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 8 | 9 | function makeGhost(options) { 10 | options = options || {}; 11 | 12 | return server(options); 13 | } 14 | 15 | module.exports = makeGhost; 16 | -------------------------------------------------------------------------------- /core/server/helpers/tpl/pagination.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/server/helpers/encode.js: -------------------------------------------------------------------------------- 1 | // # Encode Helper 2 | // 3 | // Usage: `{{encode uri}}` 4 | // 5 | // Returns URI encoded string 6 | 7 | var hbs = require('express-hbs'), 8 | encode; 9 | 10 | encode = function (context, str) { 11 | var uri = context || str; 12 | return new hbs.handlebars.SafeString(encodeURIComponent(uri)); 13 | }; 14 | 15 | module.exports = encode; 16 | -------------------------------------------------------------------------------- /core/server/helpers/title.js: -------------------------------------------------------------------------------- 1 | // # Title Helper 2 | // Usage: `{{title}}` 3 | // 4 | // Overrides the standard behaviour of `{[title}}` to ensure the content is correctly escaped 5 | 6 | var hbs = require('express-hbs'), 7 | title; 8 | 9 | title = function () { 10 | return new hbs.handlebars.SafeString(hbs.handlebars.Utils.escapeExpression(this.title || '')); 11 | }; 12 | 13 | module.exports = title; 14 | -------------------------------------------------------------------------------- /core/server/models/client.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | 3 | Client, 4 | Clients; 5 | 6 | Client = ghostBookshelf.Model.extend({ 7 | tableName: 'clients' 8 | }); 9 | 10 | Clients = ghostBookshelf.Collection.extend({ 11 | model: Client 12 | }); 13 | 14 | module.exports = { 15 | Client: ghostBookshelf.model('Client', Client), 16 | Clients: ghostBookshelf.collection('Clients', Clients) 17 | }; 18 | -------------------------------------------------------------------------------- /core/server/errors/email-error.js: -------------------------------------------------------------------------------- 1 | // # Email error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function EmailError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.code = 500; 8 | this.type = this.name; 9 | } 10 | 11 | EmailError.prototype = Object.create(Error.prototype); 12 | EmailError.prototype.name = 'EmailError'; 13 | 14 | module.exports = EmailError; 15 | -------------------------------------------------------------------------------- /core/server/errors/not-found-error.js: -------------------------------------------------------------------------------- 1 | // # Not found error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function NotFoundError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.code = 404; 8 | this.type = this.name; 9 | } 10 | 11 | NotFoundError.prototype = Object.create(Error.prototype); 12 | NotFoundError.prototype.name = 'NotFoundError'; 13 | 14 | module.exports = NotFoundError; 15 | -------------------------------------------------------------------------------- /core/server/errors/bad-request-error.js: -------------------------------------------------------------------------------- 1 | // # Bad request error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function BadRequestError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.code = 400; 8 | this.type = this.name; 9 | } 10 | 11 | BadRequestError.prototype = Object.create(Error.prototype); 12 | BadRequestError.prototype.name = 'BadRequestError'; 13 | 14 | module.exports = BadRequestError; 15 | -------------------------------------------------------------------------------- /core/server/errors/unauthorized-error.js: -------------------------------------------------------------------------------- 1 | // # Unauthorized error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function UnauthorizedError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.code = 401; 8 | this.type = this.name; 9 | } 10 | 11 | UnauthorizedError.prototype = Object.create(Error.prototype); 12 | UnauthorizedError.prototype.name = 'UnauthorizedError'; 13 | 14 | module.exports = UnauthorizedError; 15 | -------------------------------------------------------------------------------- /core/server/errors/no-permission-error.js: -------------------------------------------------------------------------------- 1 | // # No Permission Error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function NoPermissionError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.code = 403; 8 | this.type = this.name; 9 | } 10 | 11 | NoPermissionError.prototype = Object.create(Error.prototype); 12 | NoPermissionError.prototype.name = 'NoPermissionError'; 13 | 14 | module.exports = NoPermissionError; 15 | -------------------------------------------------------------------------------- /core/server/errors/internal-server-error.js: -------------------------------------------------------------------------------- 1 | // # Internal Server Error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function InternalServerError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.code = 500; 8 | this.type = this.name; 9 | } 10 | 11 | InternalServerError.prototype = Object.create(Error.prototype); 12 | InternalServerError.prototype.name = 'InternalServerError'; 13 | 14 | module.exports = InternalServerError; 15 | -------------------------------------------------------------------------------- /core/server/errors/unsupported-media-type-error.js: -------------------------------------------------------------------------------- 1 | // # Unsupported Media Type 2 | // Custom error class with status code and type prefilled. 3 | 4 | function UnsupportedMediaTypeError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.code = 415; 8 | this.type = this.name; 9 | } 10 | 11 | UnsupportedMediaTypeError.prototype = Object.create(Error.prototype); 12 | UnsupportedMediaTypeError.prototype.name = 'UnsupportedMediaTypeError'; 13 | 14 | module.exports = UnsupportedMediaTypeError; 15 | -------------------------------------------------------------------------------- /core/server/models/accesstoken.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | Basetoken = require('./basetoken'), 3 | 4 | Accesstoken, 5 | Accesstokens; 6 | 7 | Accesstoken = Basetoken.extend({ 8 | tableName: 'accesstokens' 9 | }); 10 | 11 | Accesstokens = ghostBookshelf.Collection.extend({ 12 | model: Accesstoken 13 | }); 14 | 15 | module.exports = { 16 | Accesstoken: ghostBookshelf.model('Accesstoken', Accesstoken), 17 | Accesstokens: ghostBookshelf.collection('Accesstokens', Accesstokens) 18 | }; 19 | -------------------------------------------------------------------------------- /core/server/errors/request-too-large-error.js: -------------------------------------------------------------------------------- 1 | // # Request Entity Too Large Error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function RequestEntityTooLargeError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.code = 413; 8 | this.type = this.name; 9 | } 10 | 11 | RequestEntityTooLargeError.prototype = Object.create(Error.prototype); 12 | RequestEntityTooLargeError.prototype.name = 'RequestEntityTooLargeError'; 13 | 14 | module.exports = RequestEntityTooLargeError; 15 | -------------------------------------------------------------------------------- /core/server/models/refreshtoken.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | Basetoken = require('./basetoken'), 3 | 4 | Refreshtoken, 5 | Refreshtokens; 6 | 7 | Refreshtoken = Basetoken.extend({ 8 | tableName: 'refreshtokens' 9 | }); 10 | 11 | Refreshtokens = ghostBookshelf.Collection.extend({ 12 | model: Refreshtoken 13 | }); 14 | 15 | module.exports = { 16 | Refreshtoken: ghostBookshelf.model('Refreshtoken', Refreshtoken), 17 | Refreshtokens: ghostBookshelf.collection('Refreshtokens', Refreshtokens) 18 | }; 19 | -------------------------------------------------------------------------------- /core/server/models/app-field.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | AppField, 3 | AppFields; 4 | 5 | AppField = ghostBookshelf.Model.extend({ 6 | tableName: 'app_fields', 7 | 8 | post: function () { 9 | return this.morphOne('Post', 'relatable'); 10 | } 11 | }); 12 | 13 | AppFields = ghostBookshelf.Collection.extend({ 14 | model: AppField 15 | }); 16 | 17 | module.exports = { 18 | AppField: ghostBookshelf.model('AppField', AppField), 19 | AppFields: ghostBookshelf.collection('AppFields', AppFields) 20 | }; 21 | -------------------------------------------------------------------------------- /core/server/models/app-setting.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | AppSetting, 3 | AppSettings; 4 | 5 | AppSetting = ghostBookshelf.Model.extend({ 6 | tableName: 'app_settings', 7 | 8 | app: function () { 9 | return this.belongsTo('App'); 10 | } 11 | }); 12 | 13 | AppSettings = ghostBookshelf.Collection.extend({ 14 | model: AppSetting 15 | }); 16 | 17 | module.exports = { 18 | AppSetting: ghostBookshelf.model('AppSetting', AppSetting), 19 | AppSettings: ghostBookshelf.collection('AppSettings', AppSettings) 20 | }; 21 | -------------------------------------------------------------------------------- /core/server/errors/validation-error.js: -------------------------------------------------------------------------------- 1 | // # Validation Error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function ValidationError(message, offendingProperty) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.code = 422; 8 | if (offendingProperty) { 9 | this.property = offendingProperty; 10 | } 11 | this.type = this.name; 12 | } 13 | 14 | ValidationError.prototype = Object.create(Error.prototype); 15 | ValidationError.prototype.name = 'ValidationError'; 16 | 17 | module.exports = ValidationError; 18 | -------------------------------------------------------------------------------- /core/server/errors/data-import-error.js: -------------------------------------------------------------------------------- 1 | // # Data import error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function DataImportError(message, offendingProperty, value) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.code = 500; 8 | this.type = this.name; 9 | this.property = offendingProperty || undefined; 10 | this.value = value || undefined; 11 | } 12 | 13 | DataImportError.prototype = Object.create(Error.prototype); 14 | DataImportError.prototype.name = 'DataImportError'; 15 | 16 | module.exports = DataImportError; 17 | -------------------------------------------------------------------------------- /content/themes/casper/assets/js/ja.js: -------------------------------------------------------------------------------- 1 | (function (i, s, o, g, r, a, m) { 2 | i['GoogleAnalyticsObject'] = r; 3 | i[r] = i[r] || function () { 4 | (i[r].q = i[r].q || []).push(arguments) 5 | }, i[r].l = 1 * new Date(); 6 | a = s.createElement(o), 7 | m = s.getElementsByTagName(o)[0]; 8 | a.async = 1; 9 | a.src = g; 10 | m.parentNode.insertBefore(a, m) 11 | })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); 12 | 13 | ga('create', 'UA-28954757-1', 'auto'); 14 | ga('send', 'pageview'); -------------------------------------------------------------------------------- /content/themes/casper/index_edits.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ghost", 3 | "dependencies": { 4 | "backbone": "1.0.0", 5 | "codemirror": "4.0.1", 6 | "Countable": "2.0.2", 7 | "fastclick": "1.0.0", 8 | "ghost-ui": "0.1.3", 9 | "handlebars": "1.3.0", 10 | "jquery": "1.11.0", 11 | "jquery-file-upload": "9.5.6", 12 | "jquery-hammerjs": "1.0.1", 13 | "jquery-ui": "1.10.4", 14 | "lodash": "2.4.1", 15 | "moment": "2.4.0", 16 | "nprogress": "0.1.2", 17 | "showdown": "https://github.com/ErisDS/showdown.git#v0.3.2-ghost", 18 | "validator-js": "3.4.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/server/utils/pipeline.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | 3 | function pipeline(tasks /* initial arguments */) { 4 | var args = Array.prototype.slice.call(arguments, 1), 5 | 6 | runTask = function (task, args) { 7 | runTask = function (task, arg) { 8 | return task(arg); 9 | }; 10 | 11 | return task.apply(null, args); 12 | }; 13 | 14 | return Promise.all(tasks).reduce(function (arg, task) { 15 | return Promise.resolve(runTask(task, arg)).then(function (result) { 16 | return result; 17 | }); 18 | }, args); 19 | } 20 | 21 | module.exports = pipeline; 22 | -------------------------------------------------------------------------------- /core/server/helpers/utils.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | utils; 3 | 4 | utils = { 5 | assetTemplate: _.template('<%= source %>?v=<%= version %>'), 6 | linkTemplate: _.template('<%= text %>'), 7 | scriptTemplate: _.template(''), 8 | isProduction: process.env.NODE_ENV === 'production', 9 | scriptFiles: { 10 | production: [ 11 | 'vendor.min.js', 12 | 'ghost.min.js' 13 | ], 14 | development: [ 15 | 'vendor-dev.js', 16 | 'templates-dev.js', 17 | 'ghost-dev.js' 18 | ] 19 | } 20 | }; 21 | 22 | module.exports = utils; 23 | -------------------------------------------------------------------------------- /core/server/models/permission.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | 3 | Permission, 4 | Permissions; 5 | 6 | Permission = ghostBookshelf.Model.extend({ 7 | 8 | tableName: 'permissions', 9 | 10 | roles: function () { 11 | return this.belongsToMany('Role'); 12 | }, 13 | 14 | users: function () { 15 | return this.belongsToMany('User'); 16 | }, 17 | 18 | apps: function () { 19 | return this.belongsToMany('App'); 20 | } 21 | }); 22 | 23 | Permissions = ghostBookshelf.Collection.extend({ 24 | model: Permission 25 | }); 26 | 27 | module.exports = { 28 | Permission: ghostBookshelf.model('Permission', Permission), 29 | Permissions: ghostBookshelf.collection('Permissions', Permissions) 30 | }; 31 | -------------------------------------------------------------------------------- /npm-debug.log: -------------------------------------------------------------------------------- 1 | 0 info it worked if it ends with ok 2 | 1 verbose cli [ '/usr/bin/node', '/usr/bin/npm', 'start', 'production' ] 3 | 2 info using npm@1.4.28 4 | 3 info using node@v0.10.32 5 | 4 verbose node symlink /usr/bin/node 6 | 5 error Error: ENOENT, open '/home/kevin/ghost/node_modules/production/package.json' 7 | 6 error If you need help, you may report this *entire* log, 8 | 6 error including the npm and node versions, at: 9 | 6 error 10 | 7 error System Linux 3.13.0-36-generic 11 | 8 error command "/usr/bin/node" "/usr/bin/npm" "start" "production" 12 | 9 error cwd /home/kevin/ghost 13 | 10 error node -v v0.10.32 14 | 11 error npm -v 1.4.28 15 | 12 error path /home/kevin/ghost/node_modules/production/package.json 16 | 13 error code ENOENT 17 | 14 error errno 34 18 | 15 verbose exit [ 34, true ] 19 | -------------------------------------------------------------------------------- /core/server/storage/index.js: -------------------------------------------------------------------------------- 1 | var errors = require('../errors'), 2 | storage = {}; 3 | 4 | function getStorage(storageChoice) { 5 | // TODO: this is where the check for storage apps should go 6 | // Local file system is the default. Fow now that is all we support. 7 | storageChoice = 'local-file-store'; 8 | 9 | if (storage[storageChoice]) { 10 | return storage[storageChoice]; 11 | } 12 | 13 | try { 14 | // TODO: determine if storage has all the necessary methods. 15 | storage[storageChoice] = require('./' + storageChoice); 16 | } catch (e) { 17 | errors.logError(e); 18 | } 19 | 20 | // Instantiate and cache the storage module instance. 21 | storage[storageChoice] = new storage[storageChoice](); 22 | 23 | return storage[storageChoice]; 24 | } 25 | 26 | module.exports.getStorage = getStorage; 27 | -------------------------------------------------------------------------------- /core/server/helpers/ghost_script_tags.js: -------------------------------------------------------------------------------- 1 | // # Ghost Script Tags Helpers 2 | // Used in the ghost admin only 3 | // 4 | // We use the name ghost_script_tags to match the helper for consistency: 5 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 6 | 7 | var _ = require('lodash'), 8 | utils = require('./utils'), 9 | config = require('../config'), 10 | ghost_script_tags; 11 | 12 | ghost_script_tags = function () { 13 | var scriptList = utils.isProduction ? utils.scriptFiles.production : utils.scriptFiles.development; 14 | 15 | scriptList = _.map(scriptList, function (fileName) { 16 | return utils.scriptTemplate({ 17 | source: config.paths.subdir + '/ghost/scripts/' + fileName, 18 | version: config.assetHash 19 | }); 20 | }); 21 | 22 | return scriptList.join(''); 23 | }; 24 | 25 | module.exports = ghost_script_tags; 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // # Ghost bootloader 2 | // Orchestrates the loading of Ghost 3 | // When run from command line. 4 | 5 | var express, 6 | ghost, 7 | parentApp, 8 | errors; 9 | 10 | // Make sure dependencies are installed and file system permissions are correct. 11 | require('./core/server/utils/startup-check').check(); 12 | 13 | // Proceed with startup 14 | express = require('express'); 15 | ghost = require('./core'); 16 | errors = require('./core/server/errors'); 17 | 18 | // Create our parent express app instance. 19 | parentApp = express(); 20 | 21 | ghost().then(function (ghostServer) { 22 | // Mount our ghost instance on our desired subdirectory path if it exists. 23 | parentApp.use(ghostServer.config.paths.subdir, ghostServer.rootApp); 24 | 25 | // Let ghost handle starting our server instance. 26 | ghostServer.start(parentApp); 27 | }).catch(function (err) { 28 | errors.logErrorAndExit(err, err.context, err.help); 29 | }); 30 | -------------------------------------------------------------------------------- /core/server/helpers/is.js: -------------------------------------------------------------------------------- 1 | // # Is Helper 2 | // Usage: `{{#is "paged"}}`, `{{#is "index, paged"}}` 3 | // Checks whether we're in a given context. 4 | var _ = require('lodash'), 5 | errors = require('../errors'), 6 | is; 7 | 8 | is = function (context, options) { 9 | options = options || {}; 10 | 11 | var currentContext = options.data.root.context; 12 | 13 | if (!_.isString(context)) { 14 | errors.logWarn('Invalid or no attribute given to is helper'); 15 | return; 16 | } 17 | 18 | function evaluateContext(expr) { 19 | return expr.split(',').map(function (v) { 20 | return v.trim(); 21 | }).reduce(function (p, c) { 22 | return p || _.contains(currentContext, c); 23 | }, false); 24 | } 25 | 26 | if (evaluateContext(context)) { 27 | return options.fn(this); 28 | } 29 | return options.inverse(this); 30 | }; 31 | 32 | module.exports = is; 33 | -------------------------------------------------------------------------------- /content/themes/kevgriffin/page.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 | 3 | {{! This is a page template. A page outputs content just like any other post, and has all the same 4 | attributes by default, but you can also customise it to behave differently if you prefer. }} 5 | 6 |
7 | 8 | 31 | 32 |
33 | -------------------------------------------------------------------------------- /core/server/api/tags.js: -------------------------------------------------------------------------------- 1 | // # Tag API 2 | // RESTful API for the Tag resource 3 | var Promise = require('bluebird'), 4 | canThis = require('../permissions').canThis, 5 | dataProvider = require('../models'), 6 | errors = require('../errors'), 7 | tags; 8 | 9 | /** 10 | * ## Tags API Methods 11 | * 12 | * **See:** [API Methods](index.js.html#api%20methods) 13 | */ 14 | tags = { 15 | /** 16 | * ### Browse 17 | * @param {{context}} options 18 | * @returns {Promise(Tags)} 19 | */ 20 | browse: function browse(options) { 21 | return canThis(options.context).browse.tag().then(function () { 22 | return dataProvider.Tag.findAll(options).then(function (result) { 23 | return {tags: result.toJSON()}; 24 | }); 25 | }, function () { 26 | return Promise.reject(new errors.NoPermissionError('You do not have permission to browse tags.')); 27 | }); 28 | } 29 | }; 30 | 31 | module.exports = tags; 32 | -------------------------------------------------------------------------------- /content/themes/casper/partials/loop.hbs: -------------------------------------------------------------------------------- 1 | {{! Previous/next page links - only displayed on page 2+ }} 2 |
3 | {{pagination}} 4 |
5 | 6 | {{! This is the post loop - each post will be output using this markup }} 7 | {{#foreach posts}} 8 |
9 |
10 |

{{{title}}}

11 |
12 |
13 |

{{excerpt words="26"}} »

14 |
15 |
16 | {{#if author.image}}Author image{{/if}} 17 | {{author}} 18 | {{tags prefix=" on "}} 19 | 20 |
21 |
22 | {{/foreach}} 23 | 24 | {{! Previous/next page links - displayed on every page }} 25 | {{pagination}} 26 | -------------------------------------------------------------------------------- /content/themes/casper/tag.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 | {{! The tag above means - insert everything in this file into the {body} of the default.hbs template }} 3 | 4 | {{! The big featured header }} 5 |
6 | 10 |
11 |
12 |

{{tag.name}}

13 |

A {{pagination.total}}-post collection

14 |
15 |
16 |
17 | 18 | {{! The main content area on the homepage }} 19 |
20 | 21 | {{! The tag below includes the post loop - partials/loop.hbs }} 22 | {{> "loop"}} 23 | 24 |
25 | -------------------------------------------------------------------------------- /core/server/helpers/url.js: -------------------------------------------------------------------------------- 1 | // # URL helper 2 | // Usage: `{{url}}`, `{{url absolute="true"}}` 3 | // 4 | // Returns the URL for the current object scope i.e. If inside a post scope will return post permalink 5 | // `absolute` flag outputs absolute URL, else URL is relative 6 | 7 | var Promise = require('bluebird'), 8 | config = require('../config'), 9 | api = require('../api'), 10 | schema = require('../data/schema').checks, 11 | url; 12 | 13 | url = function (options) { 14 | var absolute = options && options.hash.absolute; 15 | 16 | if (schema.isPost(this)) { 17 | return config.urlForPost(api.settings, this, absolute); 18 | } 19 | 20 | if (schema.isTag(this)) { 21 | return Promise.resolve(config.urlFor('tag', {tag: this}, absolute)); 22 | } 23 | 24 | if (schema.isUser(this)) { 25 | return Promise.resolve(config.urlFor('author', {author: this}, absolute)); 26 | } 27 | 28 | return Promise.resolve(config.urlFor(this, absolute)); 29 | }; 30 | 31 | module.exports = url; 32 | -------------------------------------------------------------------------------- /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, @ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-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. 23 | -------------------------------------------------------------------------------- /core/server/helpers/date.js: -------------------------------------------------------------------------------- 1 | // # Date Helper 2 | // Usage: `{{date format="DD MM, YYYY"}}`, `{{date updated_at format="DD MM, YYYY"}}` 3 | // 4 | // Formats a date using moment.js. Formats published_at by default but will also take a date as a parameter 5 | 6 | var moment = require('moment'), 7 | date; 8 | 9 | date = function (context, options) { 10 | if (!options && context.hasOwnProperty('hash')) { 11 | options = context; 12 | context = undefined; 13 | 14 | // set to published_at by default, if it's available 15 | // otherwise, this will print the current date 16 | if (this.published_at) { 17 | context = this.published_at; 18 | } 19 | } 20 | 21 | // ensure that context is undefined, not null, as that can cause errors 22 | context = context === null ? undefined : context; 23 | 24 | var f = options.hash.format || 'MMM Do, YYYY', 25 | timeago = options.hash.timeago, 26 | date; 27 | 28 | if (timeago) { 29 | date = moment(context).fromNow(); 30 | } else { 31 | date = moment(context).format(f); 32 | } 33 | return date; 34 | }; 35 | 36 | module.exports = date; 37 | -------------------------------------------------------------------------------- /content/themes/kevgriffin/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. 23 | -------------------------------------------------------------------------------- /content/themes/casper/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-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. 23 | -------------------------------------------------------------------------------- /core/server/helpers/ghost_foot.js: -------------------------------------------------------------------------------- 1 | // # Ghost Foot Helper 2 | // Usage: `{{ghost_foot}}` 3 | // 4 | // Outputs scripts and other assets at the bottom of a Ghost theme 5 | // 6 | // We use the name ghost_foot to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var hbs = require('express-hbs'), 10 | _ = require('lodash'), 11 | config = require('../config'), 12 | filters = require('../filters'), 13 | utils = require('./utils'), 14 | ghost_foot; 15 | 16 | ghost_foot = function (options) { 17 | /*jshint unused:false*/ 18 | var jquery = utils.isProduction ? 'jquery.min.js' : 'jquery.js', 19 | foot = []; 20 | 21 | foot.push(utils.scriptTemplate({ 22 | source: config.paths.subdir + '/public/' + jquery, 23 | version: config.assetHash 24 | })); 25 | 26 | return filters.doFilter('ghost_foot', foot).then(function (foot) { 27 | var footString = _.reduce(foot, function (memo, item) { return memo + '\n' + item; }, '\n'); 28 | return new hbs.handlebars.SafeString(footString.trim()); 29 | }); 30 | }; 31 | 32 | module.exports = ghost_foot; 33 | -------------------------------------------------------------------------------- /core/server/views/error.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 |
3 |
4 |
5 | 6 |
7 |
8 |

{{code}}

9 |

{{message}}

10 | Go to the front page → 11 |
12 |
13 |
14 | {{#if stack}} 15 |
16 |

Stack Trace

17 |

{{message}}

18 | 27 |
28 | {{/if}} 29 | -------------------------------------------------------------------------------- /content/themes/kevgriffin/README.md: -------------------------------------------------------------------------------- 1 | # Casper 2 | 3 | The default theme for [Ghost](http://github.com/tryghost/ghost/). 4 | 5 | ## Copyright & License 6 | 7 | Copyright (C) 2014 Ghost Foundation - Released under the MIT License. 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /core/server/helpers/excerpt.js: -------------------------------------------------------------------------------- 1 | // # Excerpt Helper 2 | // Usage: `{{excerpt}}`, `{{excerpt words="50"}}`, `{{excerpt characters="256"}}` 3 | // 4 | // Attempts to remove all HTML from the string, and then shortens the result according to the provided option. 5 | // 6 | // Defaults to words="50" 7 | 8 | var hbs = require('express-hbs'), 9 | _ = require('lodash'), 10 | downsize = require('downsize'), 11 | excerpt; 12 | 13 | excerpt = function (options) { 14 | var truncateOptions = (options || {}).hash || {}, 15 | excerpt; 16 | 17 | truncateOptions = _.pick(truncateOptions, ['words', 'characters']); 18 | _.keys(truncateOptions).map(function (key) { 19 | truncateOptions[key] = parseInt(truncateOptions[key], 10); 20 | }); 21 | 22 | /*jslint regexp:true */ 23 | excerpt = String(this.html).replace(/<\/?[^>]+>/gi, ''); 24 | excerpt = excerpt.replace(/(\r\n|\n|\r)+/gm, ' '); 25 | /*jslint regexp:false */ 26 | 27 | if (!truncateOptions.words && !truncateOptions.characters) { 28 | truncateOptions.words = 50; 29 | } 30 | 31 | return new hbs.handlebars.SafeString( 32 | downsize(excerpt, truncateOptions) 33 | ); 34 | }; 35 | 36 | module.exports = excerpt; 37 | -------------------------------------------------------------------------------- /core/server/data/utils/clients/sqlite3.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | config = require('../../../config/index'), 3 | 4 | // private 5 | doRaw, 6 | 7 | // public 8 | getTables, 9 | getIndexes, 10 | getColumns; 11 | 12 | doRaw = function doRaw(query, fn) { 13 | return config.database.knex.raw(query).then(function (response) { 14 | return fn(response); 15 | }); 16 | }; 17 | 18 | getTables = function getTables() { 19 | return doRaw('select * from sqlite_master where type = "table"', function (response) { 20 | return _.reject(_.pluck(response, 'tbl_name'), function (name) { 21 | return name === 'sqlite_sequence'; 22 | }); 23 | }); 24 | }; 25 | 26 | getIndexes = function getIndexes(table) { 27 | return doRaw('pragma index_list("' + table + '")', function (response) { 28 | return _.flatten(_.pluck(response, 'name')); 29 | }); 30 | }; 31 | 32 | getColumns = function getColumns(table) { 33 | return doRaw('pragma table_info("' + table + '")', function (response) { 34 | return _.flatten(_.pluck(response, 'name')); 35 | }); 36 | }; 37 | 38 | module.exports = { 39 | getTables: getTables, 40 | getIndexes: getIndexes, 41 | getColumns: getColumns 42 | }; 43 | -------------------------------------------------------------------------------- /core/server/helpers/asset.js: -------------------------------------------------------------------------------- 1 | // # Asset helper 2 | // Usage: `{{asset "css/screen.css"}}`, `{{asset "css/screen.css" ghost="true"}}` 3 | // 4 | // Returns the path to the specified asset. The ghost flag outputs the asset path for the Ghost admin 5 | 6 | var hbs = require('express-hbs'), 7 | config = require('../config'), 8 | utils = require('./utils'), 9 | asset; 10 | 11 | asset = function (context, options) { 12 | var output = '', 13 | isAdmin = options && options.hash && options.hash.ghost; 14 | 15 | output += config.paths.subdir + '/'; 16 | 17 | if (!context.match(/^favicon\.ico$/) && !context.match(/^shared/) && !context.match(/^asset/)) { 18 | if (isAdmin) { 19 | output += 'ghost/'; 20 | } else { 21 | output += 'assets/'; 22 | } 23 | } 24 | 25 | // Get rid of any leading slash on the context 26 | context = context.replace(/^\//, ''); 27 | output += context; 28 | 29 | if (!context.match(/^favicon\.ico$/)) { 30 | output = utils.assetTemplate({ 31 | source: output, 32 | version: config.assetHash 33 | }); 34 | } 35 | 36 | return new hbs.handlebars.SafeString(output); 37 | }; 38 | 39 | module.exports = asset; 40 | -------------------------------------------------------------------------------- /core/server/api/utils.js: -------------------------------------------------------------------------------- 1 | // # API Utils 2 | // Shared helpers for working with the API 3 | var Promise = require('bluebird'), 4 | _ = require('lodash'), 5 | errors = require('../errors'), 6 | utils; 7 | 8 | utils = { 9 | /** 10 | * ### Check Object 11 | * Check an object passed to the API is in the correct format 12 | * 13 | * @param {Object} object 14 | * @param {String} docName 15 | * @returns {Promise(Object)} resolves to the original object if it checks out 16 | */ 17 | checkObject: function (object, docName) { 18 | if (_.isEmpty(object) || _.isEmpty(object[docName]) || _.isEmpty(object[docName][0])) { 19 | return errors.logAndRejectError(new errors.BadRequestError('No root key (\'' + docName + '\') provided.')); 20 | } 21 | 22 | // convert author property to author_id to match the name in the database 23 | // TODO: rename object in database 24 | if (docName === 'posts') { 25 | if (object.posts[0].hasOwnProperty('author')) { 26 | object.posts[0].author_id = object.posts[0].author; 27 | delete object.posts[0].author; 28 | } 29 | } 30 | return Promise.resolve(object); 31 | } 32 | }; 33 | 34 | module.exports = utils; 35 | -------------------------------------------------------------------------------- /content/themes/casper/README.md: -------------------------------------------------------------------------------- 1 | # Casper 2 | 3 | The default theme for [Ghost](http://github.com/tryghost/ghost/). 4 | 5 | To download, visit the [releases](https://github.com/TryGhost/Casper/releases) page. 6 | 7 | ## Copyright & License 8 | 9 | Copyright (c) 2013-2014 Ghost Foundation - Released under the MIT License. 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /core/server/storage/base.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'), 2 | path = require('path'); 3 | 4 | function StorageBase() { 5 | } 6 | 7 | StorageBase.prototype.getTargetDir = function (baseDir) { 8 | var m = moment(new Date().getTime()), 9 | month = m.format('MM'), 10 | year = m.format('YYYY'); 11 | 12 | if (baseDir) { 13 | return path.join(baseDir, year, month); 14 | } 15 | 16 | return path.join(year, month); 17 | }; 18 | 19 | StorageBase.prototype.generateUnique = function (store, dir, name, ext, i) { 20 | var self = this, 21 | filename, 22 | append = ''; 23 | 24 | if (i) { 25 | append = '-' + i; 26 | } 27 | 28 | filename = path.join(dir, name + append + ext); 29 | 30 | return store.exists(filename).then(function (exists) { 31 | if (exists) { 32 | i = i + 1; 33 | return self.generateUnique(store, dir, name, ext, i); 34 | } else { 35 | return filename; 36 | } 37 | }); 38 | }; 39 | 40 | StorageBase.prototype.getUniqueFileName = function (store, image, targetDir) { 41 | var ext = path.extname(image.name), 42 | name = path.basename(image.name, ext).replace(/[\W]/gi, '-'), 43 | self = this; 44 | 45 | return self.generateUnique(store, targetDir, name, ext, 0); 46 | }; 47 | 48 | module.exports = StorageBase; 49 | -------------------------------------------------------------------------------- /content/themes/casper/post_edits.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 27 | 29 | comments powered by Disqus 30 | -------------------------------------------------------------------------------- /core/server/helpers/meta_description.js: -------------------------------------------------------------------------------- 1 | // # Meta Description Helper 2 | // Usage: `{{meta_description}}` 3 | // 4 | // Page description used for sharing and SEO 5 | // 6 | // We use the name meta_description to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var _ = require('lodash'), 10 | config = require('../config'), 11 | filters = require('../filters'), 12 | meta_description; 13 | 14 | meta_description = function () { 15 | var description, 16 | blog; 17 | 18 | if (_.isString(this.relativeUrl)) { 19 | blog = config.theme; 20 | if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '') { 21 | description = blog.description; 22 | } else if (this.author) { 23 | description = /\/page\//.test(this.relativeUrl) ? '' : this.author.bio; 24 | } else if (this.tag || /\/page\//.test(this.relativeUrl)) { 25 | description = ''; 26 | } else if (this.post) { 27 | description = _.isEmpty(this.post.meta_description) ? '' : this.post.meta_description; 28 | } 29 | } 30 | 31 | return filters.doFilter('meta_description', description).then(function (description) { 32 | description = description || ''; 33 | return description.trim(); 34 | }); 35 | }; 36 | 37 | module.exports = meta_description; 38 | -------------------------------------------------------------------------------- /core/server/helpers/post_class.js: -------------------------------------------------------------------------------- 1 | // # Post Class Helper 2 | // Usage: `{{post_class}}` 3 | // 4 | // Output classes for the body element 5 | // 6 | // We use the name body_class to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var hbs = require('express-hbs'), 10 | _ = require('lodash'), 11 | filters = require('../filters'), 12 | post_class; 13 | 14 | post_class = function (options) { 15 | /*jshint unused:false*/ 16 | var classes = ['post'], 17 | tags = this.post && this.post.tags ? this.post.tags : this.tags || [], 18 | featured = this.post && this.post.featured ? this.post.featured : this.featured || false, 19 | page = this.post && this.post.page ? this.post.page : this.page || false; 20 | 21 | if (tags) { 22 | classes = classes.concat(tags.map(function (tag) { return 'tag-' + tag.slug; })); 23 | } 24 | 25 | if (featured) { 26 | classes.push('featured'); 27 | } 28 | 29 | if (page) { 30 | classes.push('page'); 31 | } 32 | 33 | return filters.doFilter('post_class', classes).then(function (classes) { 34 | var classString = _.reduce(classes, function (memo, item) { return memo + ' ' + item; }, ''); 35 | return new hbs.handlebars.SafeString(classString.trim()); 36 | }); 37 | }; 38 | 39 | module.exports = post_class; 40 | -------------------------------------------------------------------------------- /core/server/helpers/plural.js: -------------------------------------------------------------------------------- 1 | // # Plural Helper 2 | // Usage: `{{plural 0 empty='No posts' singular='% post' plural='% posts'}}` 3 | // 4 | // pluralises strings depending on item count 5 | // 6 | // The 1st argument is the numeric variable which the helper operates on 7 | // The 2nd argument is the string that will be output if the variable's value is 0 8 | // The 3rd argument is the string that will be output if the variable's value is 1 9 | // The 4th argument is the string that will be output if the variable's value is 2+ 10 | 11 | var hbs = require('express-hbs'), 12 | errors = require('../errors'), 13 | _ = require('lodash'), 14 | plural; 15 | 16 | plural = function (context, options) { 17 | if (_.isUndefined(options.hash) || _.isUndefined(options.hash.empty) || 18 | _.isUndefined(options.hash.singular) || _.isUndefined(options.hash.plural)) { 19 | return errors.logAndThrowError('All values must be defined for empty, singular and plural'); 20 | } 21 | 22 | if (context === 0) { 23 | return new hbs.handlebars.SafeString(options.hash.empty); 24 | } else if (context === 1) { 25 | return new hbs.handlebars.SafeString(options.hash.singular.replace('%', context)); 26 | } else if (context >= 2) { 27 | return new hbs.handlebars.SafeString(options.hash.plural.replace('%', context)); 28 | } 29 | }; 30 | 31 | module.exports = plural; 32 | -------------------------------------------------------------------------------- /core/server/models/tag.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | 3 | Tag, 4 | Tags; 5 | 6 | Tag = ghostBookshelf.Model.extend({ 7 | 8 | tableName: 'tags', 9 | 10 | saving: function (newPage, attr, options) { 11 | /*jshint unused:false*/ 12 | 13 | var self = this; 14 | 15 | ghostBookshelf.Model.prototype.saving.apply(this, arguments); 16 | 17 | if (this.hasChanged('slug') || !this.get('slug')) { 18 | // Pass the new slug through the generator to strip illegal characters, detect duplicates 19 | return ghostBookshelf.Model.generateSlug(Tag, this.get('slug') || this.get('name'), 20 | {transacting: options.transacting}) 21 | .then(function (slug) { 22 | self.set({slug: slug}); 23 | }); 24 | } 25 | }, 26 | 27 | posts: function () { 28 | return this.belongsToMany('Post'); 29 | }, 30 | 31 | toJSON: function (options) { 32 | var attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options); 33 | 34 | attrs.parent = attrs.parent || attrs.parent_id; 35 | delete attrs.parent_id; 36 | 37 | return attrs; 38 | } 39 | }); 40 | 41 | Tags = ghostBookshelf.Collection.extend({ 42 | model: Tag 43 | }); 44 | 45 | module.exports = { 46 | Tag: ghostBookshelf.model('Tag', Tag), 47 | Tags: ghostBookshelf.collection('Tags', Tags) 48 | }; 49 | -------------------------------------------------------------------------------- /content/themes/casper/page.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 | {{! This is a page template. A page outputs content just like any other post, and has all the same 3 | attributes by default, but you can also customise it to behave differently if you prefer. }} 4 | 8 | 9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 | 24 |
25 |
26 |
27 | 28 | {{! Everything inside the #post tags pulls data from the post }} 29 | {{#post}} 30 | 31 |

{{title}}

32 | 33 |
34 | {{content}} 35 |
36 | 37 | {{/post}} 38 |
39 | 40 |
41 | -------------------------------------------------------------------------------- /core/server/helpers/content.js: -------------------------------------------------------------------------------- 1 | // # Content Helper 2 | // Usage: `{{content}}`, `{{content words="20"}}`, `{{content characters="256"}}` 3 | // 4 | // Turns content html into a safestring so that the user doesn't have to 5 | // escape it or tell handlebars to leave it alone with a triple-brace. 6 | // 7 | // Enables tag-safe truncation of content by characters or words. 8 | 9 | var hbs = require('express-hbs'), 10 | _ = require('lodash'), 11 | downsize = require('downsize'), 12 | downzero = require('../utils/downzero'), 13 | content; 14 | 15 | content = function (options) { 16 | var truncateOptions = (options || {}).hash || {}; 17 | truncateOptions = _.pick(truncateOptions, ['words', 'characters']); 18 | _.keys(truncateOptions).map(function (key) { 19 | truncateOptions[key] = parseInt(truncateOptions[key], 10); 20 | }); 21 | 22 | if (truncateOptions.hasOwnProperty('words') || truncateOptions.hasOwnProperty('characters')) { 23 | // Legacy function: {{content words="0"}} should return leading tags. 24 | if (truncateOptions.hasOwnProperty('words') && truncateOptions.words === 0) { 25 | return new hbs.handlebars.SafeString( 26 | downzero(this.html) 27 | ); 28 | } 29 | 30 | return new hbs.handlebars.SafeString( 31 | downsize(this.html, truncateOptions) 32 | ); 33 | } 34 | 35 | return new hbs.handlebars.SafeString(this.html); 36 | }; 37 | 38 | module.exports = content; 39 | -------------------------------------------------------------------------------- /core/server/helpers/author.js: -------------------------------------------------------------------------------- 1 | // # Author Helper 2 | // Usage: `{{author}}` OR `{{#author}}{{/author}}` 3 | // 4 | // Can be used as either an output or a block helper 5 | // 6 | // Output helper: `{{author}}` 7 | // Returns the full name of the author of a given post, or a blank string 8 | // if the author could not be determined. 9 | // 10 | // Block helper: `{{#author}}{{/author}}` 11 | // This is the default handlebars behaviour of dropping into the author object scope 12 | 13 | var hbs = require('express-hbs'), 14 | _ = require('lodash'), 15 | config = require('../config'), 16 | utils = require('./utils'), 17 | author; 18 | 19 | author = function (context, options) { 20 | if (_.isUndefined(options)) { 21 | options = context; 22 | } 23 | 24 | if (options.fn) { 25 | return hbs.handlebars.helpers['with'].call(this, this.author, options); 26 | } 27 | 28 | var autolink = _.isString(options.hash.autolink) && options.hash.autolink === 'false' ? false : true, 29 | output = ''; 30 | 31 | if (this.author && this.author.name) { 32 | if (autolink) { 33 | output = utils.linkTemplate({ 34 | url: config.urlFor('author', {author: this.author}), 35 | text: _.escape(this.author.name) 36 | }); 37 | } else { 38 | output = _.escape(this.author.name); 39 | } 40 | } 41 | 42 | return new hbs.handlebars.SafeString(output); 43 | }; 44 | 45 | module.exports = author; 46 | -------------------------------------------------------------------------------- /core/server/data/utils/clients/pg.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | config = require('../../../config/index'), 3 | 4 | // private 5 | doRawFlattenAndPluck, 6 | 7 | // public 8 | getTables, 9 | getIndexes, 10 | getColumns; 11 | 12 | doRawFlattenAndPluck = function doRaw(query, name) { 13 | return config.database.knex.raw(query).then(function (response) { 14 | return _.flatten(_.pluck(response.rows, name)); 15 | }); 16 | }; 17 | 18 | getTables = function getTables() { 19 | return doRawFlattenAndPluck( 20 | 'SELECT table_name FROM information_schema.tables WHERE table_schema = \'public\' and table_name not like \'pg_%\'', 'table_name' 21 | ); 22 | }; 23 | 24 | getIndexes = function getIndexes(table) { 25 | var selectIndexes = 'SELECT t.relname as table_name, i.relname as index_name, a.attname as column_name' + 26 | ' FROM pg_class t, pg_class i, pg_index ix, pg_attribute a' + 27 | ' WHERE t.oid = ix.indrelid and i.oid = ix.indexrelid and' + 28 | ' a.attrelid = t.oid and a.attnum = ANY(ix.indkey) and t.relname = \'' + table + '\''; 29 | 30 | return doRawFlattenAndPluck(selectIndexes, 'index_name'); 31 | }; 32 | 33 | getColumns = function getColumns(table) { 34 | var selectIndexes = 'SELECT column_name FROM information_schema.columns WHERE table_name = \'' + table + '\''; 35 | 36 | return doRawFlattenAndPluck(selectIndexes, 'column_name'); 37 | }; 38 | 39 | module.exports = { 40 | getTables: getTables, 41 | getIndexes: getIndexes, 42 | getColumns: getColumns 43 | }; 44 | -------------------------------------------------------------------------------- /content/themes/kevgriffin/index.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 | 3 | {{! The comment above "< default" means - insert everything in this file into 4 | the {body} of the default.hbs template, which contains our header/footer. }} 5 | 6 | {{! The big featured header on the homepage, with the site logo and description }} 7 |
8 |
9 |
10 | {{#if @blog.logo}}{{/if}} 11 |

{{@blog.title}}

12 |

{{@blog.description}}

13 |
14 |
15 |
16 | 17 | {{! The main content area on the homepage }} 18 |
19 | 20 | {{! Each post will be output using this markup }} 21 | {{#foreach posts}} 22 | 23 |
24 |
25 | 26 |

{{{title}}}

27 | 28 |
29 |
30 |

{{excerpt}}…

31 |
32 |
33 | 34 | {{/foreach}} 35 | 36 | {{!! After all the posts, we have the previous/next pagination links }} 37 | {{pagination}} 38 | 39 |
-------------------------------------------------------------------------------- /core/server/helpers/meta_title.js: -------------------------------------------------------------------------------- 1 | // # Meta Title Helper 2 | // Usage: `{{meta_title}}` 3 | // 4 | // Page title used for sharing and SEO 5 | // 6 | // We use the name meta_title to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var _ = require('lodash'), 10 | config = require('../config'), 11 | filters = require('../filters'), 12 | meta_title; 13 | 14 | meta_title = function (options) { 15 | /*jshint unused:false*/ 16 | var title = '', 17 | blog, 18 | page, 19 | pageString = ''; 20 | 21 | if (_.isString(this.relativeUrl)) { 22 | blog = config.theme; 23 | 24 | page = this.relativeUrl.match(/\/page\/(\d+)/); 25 | 26 | if (page) { 27 | pageString = ' - Page ' + page[1]; 28 | } 29 | 30 | if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '') { 31 | title = blog.title; 32 | } else if (this.author) { 33 | title = this.author.name + pageString + ' - ' + blog.title; 34 | } else if (this.tag) { 35 | title = this.tag.name + pageString + ' - ' + blog.title; 36 | } else if (this.post) { 37 | title = _.isEmpty(this.post.meta_title) ? this.post.title : this.post.meta_title; 38 | } else { 39 | title = blog.title + pageString; 40 | } 41 | } 42 | return filters.doFilter('meta_title', title).then(function (title) { 43 | title = title || ''; 44 | return title.trim(); 45 | }); 46 | }; 47 | 48 | module.exports = meta_title; 49 | -------------------------------------------------------------------------------- /core/client/assets/lib/touch-editor.js: -------------------------------------------------------------------------------- 1 | var createTouchEditor = function createTouchEditor() { 2 | var noop = function () {}, 3 | TouchEditor; 4 | 5 | TouchEditor = function (el, options) { 6 | /*jshint unused:false*/ 7 | this.textarea = el; 8 | this.win = { document : this.textarea }; 9 | this.ready = true; 10 | this.wrapping = document.createElement('div'); 11 | 12 | var textareaParent = this.textarea.parentNode; 13 | this.wrapping.appendChild(this.textarea); 14 | textareaParent.appendChild(this.wrapping); 15 | 16 | this.textarea.style.opacity = 1; 17 | }; 18 | 19 | TouchEditor.prototype = { 20 | setOption: function (type, handler) { 21 | if (type === 'onChange') { 22 | $(this.textarea).change(handler); 23 | } 24 | }, 25 | eachLine: function () { 26 | return []; 27 | }, 28 | getValue: function () { 29 | return this.textarea.value; 30 | }, 31 | setValue: function (code) { 32 | this.textarea.value = code; 33 | }, 34 | focus: noop, 35 | getCursor: function () { 36 | return { line: 0, ch: 0 }; 37 | }, 38 | setCursor: noop, 39 | currentLine: function () { 40 | return 0; 41 | }, 42 | cursorPosition: function () { 43 | return { character: 0 }; 44 | }, 45 | addMarkdown: noop, 46 | nthLine: noop, 47 | refresh: noop, 48 | selectLines: noop, 49 | on: noop, 50 | off: noop 51 | }; 52 | 53 | return TouchEditor; 54 | }; 55 | 56 | export default createTouchEditor; 57 | -------------------------------------------------------------------------------- /content/themes/casper/author.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 | {{! The tag above means - insert everything in this file into the {body} of the default.hbs template }} 3 | 4 | {{! The big featured header }} 5 | 6 | {{! Everything inside the #author tags pulls data from the author }} 7 | {{#author}} 8 |
9 | 13 |
14 | 15 |
16 | {{#if image}} 17 |
18 |
19 |
20 | {{/if}} 21 |

{{name}}

22 |

{{bio}}

23 |
24 | {{#if location}}{{location}}{{/if}} 25 | {{#if website}}{{website}}{{/if}} 26 | {{plural ../pagination.total empty='No posts' singular='% post' plural='% posts'}} 27 |
28 |
29 | {{/author}} 30 | 31 | {{! The main content area on the homepage }} 32 |
33 | 34 | {{! The tag below includes the post loop - partials/loop.hbs }} 35 | {{> "loop"}} 36 | 37 |
38 | -------------------------------------------------------------------------------- /core/server/helpers/page_url.js: -------------------------------------------------------------------------------- 1 | // ### Page URL Helper 2 | // 3 | // *Usage example:* 4 | // `{{page_url 2}}` 5 | // 6 | // Returns the URL for the page specified in the current object 7 | // context. 8 | // 9 | // We use the name page_url to match the helper for consistency: 10 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 11 | 12 | var config = require('../config'), 13 | errors = require('../errors'), 14 | page_url, 15 | pageUrl; 16 | 17 | page_url = function (context, block) { 18 | /*jshint unused:false*/ 19 | var url = config.paths.subdir; 20 | 21 | if (this.tagSlug !== undefined) { 22 | url += '/tag/' + this.tagSlug; 23 | } 24 | 25 | if (this.authorSlug !== undefined) { 26 | url += '/author/' + this.authorSlug; 27 | } 28 | 29 | if (context > 1) { 30 | url += '/page/' + context; 31 | } 32 | 33 | url += '/'; 34 | 35 | return url; 36 | }; 37 | 38 | // ### Page URL Helper: DEPRECATED 39 | // 40 | // *Usage example:* 41 | // `{{pageUrl 2}}` 42 | // 43 | // Returns the URL for the page specified in the current object 44 | // context. This helper is deprecated and will be removed in future versions. 45 | // 46 | pageUrl = function (context, block) { 47 | errors.logWarn('Warning: pageUrl is deprecated, please use page_url instead\n' + 48 | 'The helper pageUrl has been replaced with page_url in Ghost 0.4.2, and will be removed entirely in Ghost 0.6\n' + 49 | 'In your theme\'s pagination.hbs file, pageUrl should be renamed to page_url'); 50 | 51 | /*jshint unused:false*/ 52 | var self = this; 53 | 54 | return page_url.call(self, context, block); 55 | }; 56 | 57 | module.exports = page_url; 58 | module.exports.deprecated = pageUrl; 59 | -------------------------------------------------------------------------------- /core/server/helpers/tags.js: -------------------------------------------------------------------------------- 1 | // # Tags Helper 2 | // Usage: `{{tags}}`, `{{tags separator=' - '}}` 3 | // 4 | // Returns a string of the tags on the post. 5 | // By default, tags are separated by commas. 6 | // 7 | // Note that the standard {{#each tags}} implementation is unaffected by this helper 8 | 9 | var hbs = require('express-hbs'), 10 | _ = require('lodash'), 11 | config = require('../config'), 12 | utils = require('./utils'), 13 | tags; 14 | 15 | tags = function (options) { 16 | options = options || {}; 17 | options.hash = options.hash || {}; 18 | 19 | var autolink = options.hash && _.isString(options.hash.autolink) && options.hash.autolink === 'false' ? false : true, 20 | separator = options.hash && _.isString(options.hash.separator) ? options.hash.separator : ', ', 21 | prefix = options.hash && _.isString(options.hash.prefix) ? options.hash.prefix : '', 22 | suffix = options.hash && _.isString(options.hash.suffix) ? options.hash.suffix : '', 23 | output = ''; 24 | 25 | function createTagList(tags) { 26 | var tagNames = _.pluck(tags, 'name'); 27 | 28 | if (autolink) { 29 | return _.map(tags, function (tag) { 30 | return utils.linkTemplate({ 31 | url: config.urlFor('tag', {tag: tag}), 32 | text: _.escape(tag.name) 33 | }); 34 | }).join(separator); 35 | } 36 | return _.escape(tagNames.join(separator)); 37 | } 38 | 39 | if (this.tags && this.tags.length) { 40 | output = prefix + createTagList(this.tags) + suffix; 41 | } 42 | 43 | return new hbs.handlebars.SafeString(output); 44 | }; 45 | 46 | module.exports = tags; 47 | -------------------------------------------------------------------------------- /core/server/apps/dependencies.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('lodash'), 3 | fs = require('fs'), 4 | path = require('path'), 5 | Promise = require('bluebird'), 6 | spawn = require('child_process').spawn, 7 | win32 = process.platform === 'win32'; 8 | 9 | function AppDependencies(appPath) { 10 | this.appPath = appPath; 11 | } 12 | 13 | AppDependencies.prototype.install = function installAppDependencies() { 14 | var spawnOpts, 15 | self = this; 16 | 17 | return new Promise(function (resolve, reject) { 18 | fs.exists(path.join(self.appPath, 'package.json'), function (exists) { 19 | if (!exists) { 20 | // Nothing to do, resolve right away? 21 | resolve(); 22 | } else { 23 | // Run npm install in the app directory 24 | spawnOpts = { 25 | cwd: self.appPath 26 | }; 27 | 28 | self.spawnCommand('npm', ['install', '--production'], spawnOpts) 29 | .on('error', reject) 30 | .on('exit', function (err) { 31 | if (err) { 32 | reject(err); 33 | } 34 | 35 | resolve(); 36 | }); 37 | } 38 | }); 39 | }); 40 | }; 41 | 42 | // Normalize a command across OS and spawn it; taken from yeoman/generator 43 | AppDependencies.prototype.spawnCommand = function (command, args, opt) { 44 | var winCommand = win32 ? 'cmd' : command, 45 | winArgs = win32 ? ['/c'].concat(command, args) : args; 46 | 47 | opt = opt || {}; 48 | 49 | return spawn(winCommand, winArgs, _.defaults({stdio: 'inherit'}, opt)); 50 | }; 51 | 52 | module.exports = AppDependencies; 53 | -------------------------------------------------------------------------------- /core/server/helpers/pagination.js: -------------------------------------------------------------------------------- 1 | // ### Pagination Helper 2 | // `{{pagination}}` 3 | // Outputs previous and next buttons, along with info about the current page 4 | 5 | var _ = require('lodash'), 6 | errors = require('../errors'), 7 | template = require('./template'), 8 | pagination; 9 | 10 | pagination = function (options) { 11 | /*jshint unused:false*/ 12 | if (!_.isObject(this.pagination) || _.isFunction(this.pagination)) { 13 | return errors.logAndThrowError('pagination data is not an object or is a function'); 14 | } 15 | 16 | if (_.isUndefined(this.pagination.page) || _.isUndefined(this.pagination.pages) || 17 | _.isUndefined(this.pagination.total) || _.isUndefined(this.pagination.limit)) { 18 | return errors.logAndThrowError('All values must be defined for page, pages, limit and total'); 19 | } 20 | 21 | if ((!_.isNull(this.pagination.next) && !_.isNumber(this.pagination.next)) || 22 | (!_.isNull(this.pagination.prev) && !_.isNumber(this.pagination.prev))) { 23 | return errors.logAndThrowError('Invalid value, Next/Prev must be a number'); 24 | } 25 | 26 | if (!_.isNumber(this.pagination.page) || !_.isNumber(this.pagination.pages) || 27 | !_.isNumber(this.pagination.total) || !_.isNumber(this.pagination.limit)) { 28 | return errors.logAndThrowError('Invalid value, check page, pages, limit and total are numbers'); 29 | } 30 | 31 | var context = _.merge({}, this.pagination); 32 | 33 | if (this.tag !== undefined) { 34 | context.tagSlug = this.tag.slug; 35 | } 36 | 37 | if (this.author !== undefined) { 38 | context.authorSlug = this.author.slug; 39 | } 40 | 41 | return template.execute('pagination', context); 42 | }; 43 | 44 | module.exports = pagination; 45 | -------------------------------------------------------------------------------- /content/themes/casper/index.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 | {{! The tag above means - insert everything in this file into the {body} of the default.hbs template }} 3 | {{! The big featured header }}
4 | 11 |
12 |
13 |

{{@blog.title}}

14 |

{{@blog.description}}

15 |
16 |
17 | 18 |
19 | 20 | {{! The main content area on the homepage }} 21 |
22 | 23 |
24 |
25 |
26 |
27 | 35 |
36 |
37 |
38 | 39 | {{! The tag below includes the post loop - partials/loop.hbs }} 40 | {{> "loop"}} 41 |
42 | -------------------------------------------------------------------------------- /core/server/controllers/admin.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | api = require('../api'), 3 | errors = require('../errors'), 4 | updateCheck = require('../update-check'), 5 | config = require('../config'), 6 | adminControllers; 7 | 8 | adminControllers = { 9 | // Route: index 10 | // Path: /ghost/ 11 | // Method: GET 12 | index: function (req, res) { 13 | /*jslint unparam:true*/ 14 | 15 | function renderIndex() { 16 | res.render('default', { 17 | skip_google_fonts: config.isPrivacyDisabled('useGoogleFonts') 18 | }); 19 | } 20 | 21 | updateCheck().then(function () { 22 | return updateCheck.showUpdateNotification(); 23 | }).then(function (updateVersion) { 24 | if (!updateVersion) { 25 | return; 26 | } 27 | 28 | var notification = { 29 | type: 'success', 30 | location: 'top', 31 | dismissible: false, 32 | status: 'persistent', 33 | message: 'Ghost ' + updateVersion + 34 | ' is available! Hot Damn. Please upgrade now' 35 | }; 36 | 37 | return api.notifications.browse({context: {internal: true}}).then(function (results) { 38 | if (!_.some(results.notifications, {message: notification.message})) { 39 | return api.notifications.add({notifications: [notification]}, {context: {internal: true}}); 40 | } 41 | }); 42 | }).finally(function () { 43 | renderIndex(); 44 | }).catch(errors.logError); 45 | } 46 | }; 47 | 48 | module.exports = adminControllers; 49 | -------------------------------------------------------------------------------- /content/themes/casper/default.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{! Document Settings }} 5 | 6 | 7 | 8 | {{! Page Meta }} 9 | {{meta_title}} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {{! Styles'n'Scripts }} 18 | 19 | 20 | 21 | {{! Ghost outputs important style and meta data with this tag }} 22 | {{ghost_head}} 23 | 24 | 25 | 26 | {{! Everything else gets inserted here }} 27 | {{{body}}} 28 | 29 | 33 | 34 | {{! Ghost outputs important scripts and data with this tag }} 35 | {{ghost_foot}} 36 | 37 | {{! The main JavaScript file for Casper }} 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /core/server/helpers/has.js: -------------------------------------------------------------------------------- 1 | // # Has Helper 2 | // Usage: `{{#has tag="video, music"}}`, `{{#has author="sam, pat"}}` 3 | // 4 | // Checks if a post has a particular property 5 | 6 | var _ = require('lodash'), 7 | errors = require('../errors'), 8 | has; 9 | 10 | has = function (options) { 11 | options = options || {}; 12 | options.hash = options.hash || {}; 13 | 14 | var tags = _.pluck(this.tags, 'name'), 15 | author = this.author ? this.author.name : null, 16 | tagList = options.hash.tag || false, 17 | authorList = options.hash.author || false, 18 | tagsOk, 19 | authorOk; 20 | 21 | function evaluateTagList(expr, tags) { 22 | return expr.split(',').map(function (v) { 23 | return v.trim(); 24 | }).reduce(function (p, c) { 25 | return p || (_.findIndex(tags, function (item) { 26 | // Escape regex special characters 27 | item = item.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&'); 28 | item = new RegExp(item, 'i'); 29 | return item.test(c); 30 | }) !== -1); 31 | }, false); 32 | } 33 | 34 | function evaluateAuthorList(expr, author) { 35 | var authorList = expr.split(',').map(function (v) { 36 | return v.trim().toLocaleLowerCase(); 37 | }); 38 | 39 | return _.contains(authorList, author.toLocaleLowerCase()); 40 | } 41 | 42 | if (!tagList && !authorList) { 43 | errors.logWarn('Invalid or no attribute given to has helper'); 44 | return; 45 | } 46 | 47 | tagsOk = tagList && evaluateTagList(tagList, tags) || false; 48 | authorOk = authorList && evaluateAuthorList(authorList, author) || false; 49 | 50 | if (tagsOk || authorOk) { 51 | return options.fn(this); 52 | } 53 | return options.inverse(this); 54 | }; 55 | 56 | module.exports = has; 57 | -------------------------------------------------------------------------------- /core/server/permissions/effective.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | Models = require('../models'), 3 | errors = require('../errors'), 4 | effective; 5 | 6 | effective = { 7 | user: function (id) { 8 | return Models.User.findOne({id: id, status: 'all'}, {include: ['permissions', 'roles', 'roles.permissions']}) 9 | .then(function (foundUser) { 10 | var seenPerms = {}, 11 | rolePerms = _.map(foundUser.related('roles').models, function (role) { 12 | return role.related('permissions').models; 13 | }), 14 | allPerms = [], 15 | user = foundUser.toJSON(); 16 | 17 | rolePerms.push(foundUser.related('permissions').models); 18 | 19 | _.each(rolePerms, function (rolePermGroup) { 20 | _.each(rolePermGroup, function (perm) { 21 | var key = perm.get('action_type') + '-' + perm.get('object_type') + '-' + perm.get('object_id'); 22 | 23 | // Only add perms once 24 | if (seenPerms[key]) { 25 | return; 26 | } 27 | 28 | allPerms.push(perm); 29 | seenPerms[key] = true; 30 | }); 31 | }); 32 | 33 | return {permissions: allPerms, roles: user.roles}; 34 | }, errors.logAndThrowError); 35 | }, 36 | 37 | app: function (appName) { 38 | return Models.App.findOne({name: appName}, {withRelated: ['permissions']}) 39 | .then(function (foundApp) { 40 | if (!foundApp) { 41 | return []; 42 | } 43 | 44 | return {permissions: foundApp.related('permissions').models}; 45 | }, errors.logAndThrowError); 46 | } 47 | }; 48 | 49 | module.exports = effective; 50 | -------------------------------------------------------------------------------- /content/themes/kevgriffin/default.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{! Document Settings }} 5 | 6 | 7 | 8 | {{! Page Meta }} 9 | {{meta_title}} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{! Styles'n'Scripts }} 19 | 20 | 21 | 22 | {{! Ghost outputs important style and meta data with this tag }} 23 | {{ghost_head}} 24 | 25 | 26 | 27 | {{! Everything else gets inserted here }} 28 | {{{body}}} 29 | 30 | 37 | 38 | {{! Ghost outputs important scripts and data with this tag }} 39 | {{ghost_foot}} 40 | 41 | {{! The main JavaScript file for Casper }} 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /core/server/api/upload.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | config = require('../config'), 3 | Promise = require('bluebird'), 4 | path = require('path'), 5 | fs = require('fs-extra'), 6 | storage = require('../storage'), 7 | errors = require('../errors'), 8 | 9 | upload; 10 | 11 | function isImage(type, ext) { 12 | if (_.contains(config.uploads.contentTypes, type) && _.contains(config.uploads.extensions, ext)) { 13 | return true; 14 | } 15 | return false; 16 | } 17 | 18 | /** 19 | * ## Upload API Methods 20 | * 21 | * **See:** [API Methods](index.js.html#api%20methods) 22 | */ 23 | upload = { 24 | 25 | /** 26 | * ### Add Image 27 | * 28 | * @public 29 | * @param {{context}} options 30 | * @returns {Promise} Success 31 | */ 32 | add: function (options) { 33 | var store = storage.getStorage(), 34 | type, 35 | ext, 36 | filepath; 37 | 38 | if (!options.uploadimage || !options.uploadimage.type || !options.uploadimage.path) { 39 | return Promise.reject(new errors.NoPermissionError('Please select an image.')); 40 | } 41 | 42 | type = options.uploadimage.type; 43 | ext = path.extname(options.uploadimage.name).toLowerCase(); 44 | filepath = options.uploadimage.path; 45 | 46 | return Promise.resolve(isImage(type, ext)).then(function (result) { 47 | if (!result) { 48 | return Promise.reject(new errors.UnsupportedMediaTypeError('Please select a valid image.')); 49 | } 50 | }).then(function () { 51 | return store.save(options.uploadimage); 52 | }).then(function (url) { 53 | return url; 54 | }).finally(function () { 55 | // Remove uploaded file from tmp location 56 | return Promise.promisify(fs.unlink)(filepath); 57 | }); 58 | } 59 | }; 60 | 61 | module.exports = upload; 62 | -------------------------------------------------------------------------------- /core/server/utils/index.js: -------------------------------------------------------------------------------- 1 | var unidecode = require('unidecode'), 2 | 3 | utils, 4 | getRandomInt; 5 | 6 | /** 7 | * Return a random int, used by `utils.uid()` 8 | * 9 | * @param {Number} min 10 | * @param {Number} max 11 | * @return {Number} 12 | * @api private 13 | */ 14 | getRandomInt = function (min, max) { 15 | return Math.floor(Math.random() * (max - min + 1)) + min; 16 | }; 17 | 18 | utils = { 19 | /** 20 | * Timespans in seconds and milliseconds for better readability 21 | */ 22 | ONE_HOUR_S: 3600, 23 | ONE_DAY_S: 86400, 24 | ONE_YEAR_S: 31536000, 25 | ONE_HOUR_MS: 3600000, 26 | ONE_DAY_MS: 86400000, 27 | ONE_YEAR_MS: 31536000000, 28 | 29 | /** 30 | * Return a unique identifier with the given `len`. 31 | * 32 | * utils.uid(10); 33 | * // => "FDaS435D2z" 34 | * 35 | * @param {Number} len 36 | * @return {String} 37 | * @api private 38 | */ 39 | uid: function (len) { 40 | var buf = [], 41 | chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 42 | charlen = chars.length, 43 | i; 44 | 45 | for (i = 1; i < len; i = i + 1) { 46 | buf.push(chars[getRandomInt(0, charlen - 1)]); 47 | } 48 | 49 | return buf.join(''); 50 | }, 51 | safeString: function (string) { 52 | string = string.trim(); 53 | 54 | // Remove non ascii characters 55 | string = unidecode(string); 56 | 57 | // Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"` 58 | string = string.replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '') 59 | // Replace dots and spaces with a dash 60 | .replace(/(\s|\.)/g, '-') 61 | // Convert 2 or more dashes into a single dash 62 | .replace(/-+/g, '-') 63 | // Make the whole thing lowercase 64 | .toLowerCase(); 65 | 66 | return string; 67 | } 68 | }; 69 | 70 | module.exports = utils; 71 | -------------------------------------------------------------------------------- /core/server/data/utils/clients/mysql.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | config = require('../../../config/index'), 3 | 4 | // private 5 | doRawAndFlatten, 6 | 7 | // public 8 | getTables, 9 | getIndexes, 10 | getColumns, 11 | checkPostTable; 12 | 13 | doRawAndFlatten = function doRaw(query, flattenFn) { 14 | return config.database.knex.raw(query).then(function (response) { 15 | return _.flatten(flattenFn(response)); 16 | }); 17 | }; 18 | 19 | getTables = function getTables() { 20 | return doRawAndFlatten('show tables', function (response) { 21 | return _.map(response[0], function (entry) { return _.values(entry); }); 22 | }); 23 | }; 24 | 25 | getIndexes = function getIndexes(table) { 26 | return doRawAndFlatten('SHOW INDEXES from ' + table, function (response) { 27 | return _.pluck(response[0], 'Key_name'); 28 | }); 29 | }; 30 | 31 | getColumns = function getColumns(table) { 32 | return doRawAndFlatten('SHOW COLUMNS FROM ' + table, function (response) { 33 | return _.pluck(response[0], 'Field'); 34 | }); 35 | }; 36 | 37 | // This function changes the type of posts.html and posts.markdown columns to mediumtext. Due to 38 | // a wrong datatype in schema.js some installations using mysql could have been created using the 39 | // data type text instead of mediumtext. 40 | // For details see: https://github.com/TryGhost/Ghost/issues/1947 41 | checkPostTable = function checkPostTable() { 42 | return config.database.knex.raw('SHOW FIELDS FROM posts where Field ="html" OR Field = "markdown"').then(function (response) { 43 | return _.flatten(_.map(response[0], function (entry) { 44 | if (entry.Type.toLowerCase() !== 'mediumtext') { 45 | return config.database.knex.raw('ALTER TABLE posts MODIFY ' + entry.Field + ' MEDIUMTEXT'); 46 | } 47 | })); 48 | }); 49 | }; 50 | 51 | module.exports = { 52 | checkPostTable: checkPostTable, 53 | getTables: getTables, 54 | getIndexes: getIndexes, 55 | getColumns: getColumns 56 | }; 57 | -------------------------------------------------------------------------------- /core/server/api/slugs.js: -------------------------------------------------------------------------------- 1 | // # Slug API 2 | // RESTful API for the Slug resource 3 | var canThis = require('../permissions').canThis, 4 | dataProvider = require('../models'), 5 | errors = require('../errors'), 6 | Promise = require('bluebird'), 7 | 8 | slugs, 9 | allowedTypes; 10 | 11 | /** 12 | * ## Slugs API Methods 13 | * 14 | * **See:** [API Methods](index.js.html#api%20methods) 15 | */ 16 | slugs = { 17 | 18 | /** 19 | * ## Generate Slug 20 | * Create a unique slug for the given type and its name 21 | * 22 | * @param {{type (required), name (required), context, transacting}} options 23 | * @returns {Promise(String)} Unique string 24 | */ 25 | generate: function (options) { 26 | options = options || {}; 27 | 28 | // `allowedTypes` is used to define allowed slug types and map them against its model class counterpart 29 | allowedTypes = { 30 | post: dataProvider.Post, 31 | tag: dataProvider.Tag, 32 | user: dataProvider.User, 33 | app: dataProvider.App 34 | }; 35 | 36 | return canThis(options.context).generate.slug().then(function () { 37 | if (allowedTypes[options.type] === undefined) { 38 | return Promise.reject(new errors.BadRequestError('Unknown slug type \'' + options.type + '\'.')); 39 | } 40 | 41 | return dataProvider.Base.Model.generateSlug(allowedTypes[options.type], options.name, {status: 'all'}).then(function (slug) { 42 | if (!slug) { 43 | return Promise.reject(new errors.InternalServerError('Could not generate slug.')); 44 | } 45 | 46 | return {slugs: [{slug: slug}]}; 47 | }); 48 | }).catch(function (err) { 49 | if (err) { 50 | return Promise.reject(err); 51 | } 52 | 53 | return Promise.reject(new errors.NoPermissionError('You do not have permission to generate a slug.')); 54 | }); 55 | } 56 | 57 | }; 58 | 59 | module.exports = slugs; 60 | -------------------------------------------------------------------------------- /core/server/models/app.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | App, 3 | Apps; 4 | 5 | App = ghostBookshelf.Model.extend({ 6 | tableName: 'apps', 7 | 8 | saving: function (newPage, attr, options) { 9 | /*jshint unused:false*/ 10 | var self = this; 11 | 12 | ghostBookshelf.Model.prototype.saving.apply(this, arguments); 13 | 14 | if (this.hasChanged('slug') || !this.get('slug')) { 15 | // Pass the new slug through the generator to strip illegal characters, detect duplicates 16 | return ghostBookshelf.Model.generateSlug(App, this.get('slug') || this.get('name'), 17 | {transacting: options.transacting}) 18 | .then(function (slug) { 19 | self.set({slug: slug}); 20 | }); 21 | } 22 | }, 23 | 24 | permissions: function () { 25 | return this.belongsToMany('Permission', 'permissions_apps'); 26 | }, 27 | 28 | settings: function () { 29 | return this.belongsToMany('AppSetting', 'app_settings'); 30 | } 31 | }, { 32 | /** 33 | * Returns an array of keys permitted in a method's `options` hash, depending on the current method. 34 | * @param {String} methodName The name of the method to check valid options for. 35 | * @return {Array} Keys allowed in the `options` hash of the model's method. 36 | */ 37 | permittedOptions: function (methodName) { 38 | var options = ghostBookshelf.Model.permittedOptions(), 39 | 40 | // whitelists for the `options` hash argument on methods, by method name. 41 | // these are the only options that can be passed to Bookshelf / Knex. 42 | validOptions = { 43 | findOne: ['withRelated'] 44 | }; 45 | 46 | if (validOptions[methodName]) { 47 | options = options.concat(validOptions[methodName]); 48 | } 49 | 50 | return options; 51 | } 52 | }); 53 | 54 | Apps = ghostBookshelf.Collection.extend({ 55 | model: App 56 | }); 57 | 58 | module.exports = { 59 | App: ghostBookshelf.model('App', App), 60 | Apps: ghostBookshelf.collection('Apps', Apps) 61 | }; 62 | -------------------------------------------------------------------------------- /core/server/data/default-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": { 3 | "databaseVersion": { 4 | "defaultValue": "003" 5 | }, 6 | "dbHash": { 7 | "defaultValue": null 8 | }, 9 | "nextUpdateCheck": { 10 | "defaultValue": null 11 | }, 12 | "displayUpdateNotification": { 13 | "defaultValue": null 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 | "isNull": false, 27 | "isEmail": true 28 | } 29 | }, 30 | "logo": { 31 | "defaultValue": "" 32 | }, 33 | "cover": { 34 | "defaultValue": "" 35 | }, 36 | "defaultLang": { 37 | "defaultValue": "en_US", 38 | "validations": { 39 | "isNull": false 40 | } 41 | }, 42 | "postsPerPage": { 43 | "defaultValue": "5", 44 | "validations": { 45 | "isNull": false, 46 | "isInt": true, 47 | "isLength": [1, 1000] 48 | } 49 | }, 50 | "forceI18n": { 51 | "defaultValue": "true", 52 | "validations": { 53 | "isNull": false, 54 | "isIn": [["true", "false"]] 55 | } 56 | }, 57 | "permalinks": { 58 | "defaultValue": "/:slug/", 59 | "validations": { 60 | "matches": "^(\/:?[a-z0-9_-]+){1,5}\/$", 61 | "matches": "(:id|:slug|:year|:month|:day)", 62 | "notContains": "/ghost/" 63 | } 64 | } 65 | }, 66 | "theme": { 67 | "activeTheme": { 68 | "defaultValue": "casper" 69 | } 70 | }, 71 | "app": { 72 | "activeApps": { 73 | "defaultValue": "[]" 74 | }, 75 | "installedApps": { 76 | "defaultValue": "[]" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /core/server/api/configuration.js: -------------------------------------------------------------------------------- 1 | // # Configuration API 2 | // RESTful API for browsing the configuration 3 | var _ = require('lodash'), 4 | config = require('../config'), 5 | errors = require('../errors'), 6 | parsePackageJson = require('../require-tree').parsePackageJson, 7 | Promise = require('bluebird'), 8 | 9 | configuration; 10 | 11 | function getValidKeys() { 12 | var validKeys = { 13 | fileStorage: config.fileStorage === false ? false : true, 14 | apps: config.apps === true ? true : false, 15 | version: false, 16 | environment: process.env.NODE_ENV, 17 | database: config.database.client, 18 | mail: _.isObject(config.mail) ? config.mail.transport : '', 19 | blogUrl: config.url 20 | }; 21 | 22 | return parsePackageJson('package.json').then(function (json) { 23 | validKeys.version = json.version; 24 | return validKeys; 25 | }); 26 | } 27 | 28 | /** 29 | * ## Configuration API Methods 30 | * 31 | * **See:** [API Methods](index.js.html#api%20methods) 32 | */ 33 | configuration = { 34 | 35 | /** 36 | * ### Browse 37 | * Fetch all configuration keys 38 | * @returns {Promise(Configurations)} 39 | */ 40 | browse: function browse() { 41 | return getValidKeys().then(function (result) { 42 | return Promise.resolve({configuration: _.map(result, function (value, key) { 43 | return { 44 | key: key, 45 | value: value 46 | }; 47 | })}); 48 | }); 49 | }, 50 | 51 | /** 52 | * ### Read 53 | * 54 | */ 55 | read: function read(options) { 56 | return getValidKeys().then(function (result) { 57 | if (_.has(result, options.key)) { 58 | return Promise.resolve({configuration: [{ 59 | key: options.key, 60 | value: result[options.key] 61 | }]}); 62 | } else { 63 | return Promise.reject(new errors.NotFoundError('Invalid key')); 64 | } 65 | }); 66 | } 67 | }; 68 | 69 | module.exports = configuration; 70 | -------------------------------------------------------------------------------- /core/server/apps/permissions.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'), 3 | Promise = require('bluebird'), 4 | path = require('path'), 5 | parsePackageJson = require('../require-tree').parsePackageJson; 6 | 7 | function AppPermissions(appPath) { 8 | this.appPath = appPath; 9 | this.packagePath = path.join(this.appPath, 'package.json'); 10 | } 11 | 12 | AppPermissions.prototype.read = function () { 13 | var self = this; 14 | 15 | return this.checkPackageContentsExists().then(function (exists) { 16 | if (!exists) { 17 | // If no package.json, return default permissions 18 | return Promise.resolve(AppPermissions.DefaultPermissions); 19 | } 20 | 21 | // Read and parse the package.json 22 | return self.getPackageContents().then(function (parsed) { 23 | // If no permissions in the package.json then return the default permissions. 24 | if (!(parsed.ghost && parsed.ghost.permissions)) { 25 | return Promise.resolve(AppPermissions.DefaultPermissions); 26 | } 27 | 28 | // TODO: Validation on permissions object? 29 | 30 | return Promise.resolve(parsed.ghost.permissions); 31 | }); 32 | }); 33 | }; 34 | 35 | AppPermissions.prototype.checkPackageContentsExists = function () { 36 | var self = this; 37 | 38 | // Mostly just broken out for stubbing in unit tests 39 | return new Promise(function (resolve) { 40 | fs.exists(self.packagePath, function (exists) { 41 | resolve(exists); 42 | }); 43 | }); 44 | }; 45 | 46 | // Get the contents of the package.json in the appPath root 47 | AppPermissions.prototype.getPackageContents = function () { 48 | var messages = { 49 | errors: [], 50 | warns: [] 51 | }; 52 | 53 | return parsePackageJson(this.packagePath, messages) 54 | .then(function (parsed) { 55 | if (!parsed) { 56 | return Promise.reject(new Error(messages.errors[0].message)); 57 | } 58 | 59 | return parsed; 60 | }); 61 | }; 62 | 63 | // Default permissions for an App. 64 | AppPermissions.DefaultPermissions = { 65 | posts: ['browse', 'read'] 66 | }; 67 | 68 | module.exports = AppPermissions; 69 | -------------------------------------------------------------------------------- /core/server/data/export/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | Promise = require('bluebird'), 3 | versioning = require('../versioning'), 4 | config = require('../../config'), 5 | utils = require('../utils'), 6 | serverUtils = require('../../utils'), 7 | errors = require('../../errors'), 8 | settings = require('../../api/settings'), 9 | 10 | excludedTables = ['accesstokens', 'refreshtokens', 'clients'], 11 | exporter, 12 | exportFileName; 13 | 14 | exportFileName = function () { 15 | var datetime = (new Date()).toJSON().substring(0, 10), 16 | title = ''; 17 | 18 | return settings.read({key: 'title', context: {internal: true}}).then(function (result) { 19 | if (result) { 20 | title = serverUtils.safeString(result.settings[0].value) + '.'; 21 | } 22 | return title + 'ghost.' + datetime + '.json'; 23 | }).catch(function (err) { 24 | errors.logError(err); 25 | return 'ghost.' + datetime + '.json'; 26 | }); 27 | }; 28 | 29 | exporter = function () { 30 | return Promise.join(versioning.getDatabaseVersion(), utils.getTables()).then(function (results) { 31 | var version = results[0], 32 | tables = results[1], 33 | selectOps = _.map(tables, function (name) { 34 | if (excludedTables.indexOf(name) < 0) { 35 | return config.database.knex(name).select(); 36 | } 37 | }); 38 | 39 | return Promise.all(selectOps).then(function (tableData) { 40 | var exportData = { 41 | meta: { 42 | exported_on: new Date().getTime(), 43 | version: version 44 | }, 45 | data: { 46 | // Filled below 47 | } 48 | }; 49 | 50 | _.each(tables, function (name, i) { 51 | exportData.data[name] = tableData[i]; 52 | }); 53 | 54 | return exportData; 55 | }).catch(function (err) { 56 | errors.logAndThrowError(err, 'Error exporting data', ''); 57 | }); 58 | }); 59 | }; 60 | 61 | module.exports = exporter; 62 | module.exports.fileName = exportFileName; 63 | -------------------------------------------------------------------------------- /core/server/models/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | Promise = require('bluebird'), 3 | requireTree = require('../require-tree'), 4 | models; 5 | 6 | models = { 7 | excludeFiles: ['_messages', 'basetoken.js', 'base.js', 'index.js'], 8 | 9 | // ### init 10 | // Scan all files in this directory and then require each one and cache 11 | // the objects exported onto this `models` object so that every other 12 | // module can safely access models without fear of introducing circular 13 | // dependency issues. 14 | // @returns {Promise} 15 | init: function () { 16 | var self = this; 17 | 18 | // One off inclusion of Base file. 19 | self.Base = require('./base'); 20 | 21 | // Require all files in this directory 22 | return requireTree.readAll(__dirname, {followSymlinks: false}).then(function (modelFiles) { 23 | // For each found file, excluding those we don't want, 24 | // we will require it and cache it here. 25 | _.each(modelFiles, function (path, fileName) { 26 | // Return early if this fileName is one of the ones we want 27 | // to exclude. 28 | if (_.contains(self.excludeFiles, fileName)) { 29 | return; 30 | } 31 | 32 | // Require the file. 33 | var file = require(path); 34 | 35 | // Cache its `export` object onto this object. 36 | _.extend(self, file); 37 | }); 38 | 39 | return; 40 | }); 41 | }, 42 | // ### deleteAllContent 43 | // Delete all content from the database (posts, tags, tags_posts) 44 | deleteAllContent: function () { 45 | var self = this; 46 | 47 | return self.Post.findAll().then(function (posts) { 48 | return Promise.all(_.map(posts.toJSON(), function (post) { 49 | return self.Post.destroy({id: post.id}); 50 | })); 51 | }).then(function () { 52 | return self.Tag.findAll().then(function (tags) { 53 | return Promise.all(_.map(tags.toJSON(), function (tag) { 54 | return self.Tag.destroy({id: tag.id}); 55 | })); 56 | }); 57 | }); 58 | } 59 | }; 60 | 61 | module.exports = models; 62 | -------------------------------------------------------------------------------- /core/server/storage/local-file-store.js: -------------------------------------------------------------------------------- 1 | // # Local File System Image Storage module 2 | // The (default) module for storing images, using the local file system 3 | 4 | var express = require('express'), 5 | fs = require('fs-extra'), 6 | path = require('path'), 7 | util = require('util'), 8 | Promise = require('bluebird'), 9 | errors = require('../errors'), 10 | config = require('../config'), 11 | utils = require('../utils'), 12 | baseStore = require('./base'); 13 | 14 | function LocalFileStore() { 15 | } 16 | util.inherits(LocalFileStore, baseStore); 17 | 18 | // ### Save 19 | // Saves the image to storage (the file system) 20 | // - image is the express image object 21 | // - returns a promise which ultimately returns the full url to the uploaded image 22 | LocalFileStore.prototype.save = function (image) { 23 | var targetDir = this.getTargetDir(config.paths.imagesPath), 24 | targetFilename; 25 | 26 | return this.getUniqueFileName(this, image, targetDir).then(function (filename) { 27 | targetFilename = filename; 28 | return Promise.promisify(fs.mkdirs)(targetDir); 29 | }).then(function () { 30 | return Promise.promisify(fs.copy)(image.path, targetFilename); 31 | }).then(function () { 32 | // The src for the image must be in URI format, not a file system path, which in Windows uses \ 33 | // For local file system storage can use relative path so add a slash 34 | var fullUrl = (config.paths.subdir + '/' + config.paths.imagesRelPath + '/' + 35 | path.relative(config.paths.imagesPath, targetFilename)).replace(new RegExp('\\' + path.sep, 'g'), '/'); 36 | return fullUrl; 37 | }).catch(function (e) { 38 | errors.logError(e); 39 | return Promise.reject(e); 40 | }); 41 | }; 42 | 43 | LocalFileStore.prototype.exists = function (filename) { 44 | return new Promise(function (resolve) { 45 | fs.exists(filename, function (exists) { 46 | resolve(exists); 47 | }); 48 | }); 49 | }; 50 | 51 | // middleware for serving the files 52 | LocalFileStore.prototype.serve = function () { 53 | // For some reason send divides the max age number by 1000 54 | return express['static'](config.paths.imagesPath, {maxAge: utils.ONE_YEAR_MS}); 55 | }; 56 | 57 | module.exports = LocalFileStore; 58 | -------------------------------------------------------------------------------- /core/server/helpers/template.js: -------------------------------------------------------------------------------- 1 | var templates = {}, 2 | hbs = require('express-hbs'), 3 | errors = require('../errors'); 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 | var partial = hbs.handlebars.partials[name]; 11 | 12 | if (partial === undefined) { 13 | errors.logAndThrowError('Template ' + name + ' not found.'); 14 | return; 15 | } 16 | 17 | // If the partial view is not compiled, it compiles and saves in handlebars 18 | if (typeof partial === 'string') { 19 | hbs.registerPartial(partial); 20 | } 21 | 22 | return new hbs.handlebars.SafeString(partial(context)); 23 | }; 24 | 25 | // Given a theme object and a post object this will return 26 | // which theme template page should be used. 27 | // If given a post object that is a regular post 28 | // it will return 'post'. 29 | // If given a static post object it will return 'page'. 30 | // If given a static post object and a custom page template 31 | // exits it will return that page. 32 | templates.getThemeViewForPost = function (themePaths, post) { 33 | var customPageView = 'page-' + post.slug, 34 | view = 'post'; 35 | 36 | if (post.page) { 37 | if (themePaths.hasOwnProperty(customPageView + '.hbs')) { 38 | view = customPageView; 39 | } else if (themePaths.hasOwnProperty('page.hbs')) { 40 | view = 'page'; 41 | } 42 | } 43 | 44 | return view; 45 | }; 46 | 47 | // Given a theme object and a tag slug this will return 48 | // which theme template page should be used. 49 | // If no default or custom tag template exists then 'index' 50 | // will be returned 51 | // If no custom tag template exists but a default does then 52 | // 'tag' will be returned 53 | // If given a tag slug and a custom tag template 54 | // exits it will return that view. 55 | templates.getThemeViewForTag = function (themePaths, tag) { 56 | var customTagView = 'tag-' + tag, 57 | view = 'tag'; 58 | 59 | if (themePaths.hasOwnProperty(customTagView + '.hbs')) { 60 | view = customTagView; 61 | } else if (!themePaths.hasOwnProperty('tag.hbs')) { 62 | view = 'index'; 63 | } 64 | 65 | return view; 66 | }; 67 | 68 | module.exports = templates; 69 | -------------------------------------------------------------------------------- /core/server/views/default.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Ghost Admin 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {{#unless skip_google_fonts}} 33 | 34 | {{/unless}} 35 | 36 | 37 | 38 | 39 | {{{ghost_script_tags}}} 40 | 41 | 42 | -------------------------------------------------------------------------------- /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 | 14 | // busboy is only used for POST requests 15 | if (req.method && !/post/i.test(req.method)) { 16 | return next(); 17 | } 18 | 19 | busboy = new BusBoy({headers: req.headers}); 20 | tmpDir = os.tmpdir(); 21 | 22 | req.files = req.files || {}; 23 | req.body = req.body || {}; 24 | 25 | busboy.on('file', function (fieldname, file, filename, encoding, mimetype) { 26 | var filePath, 27 | tmpFileName, 28 | md5 = crypto.createHash('md5'); 29 | 30 | // If the filename is invalid, skip the stream 31 | if (!filename) { 32 | return file.resume(); 33 | } 34 | 35 | // Create an MD5 hash of original filename 36 | md5.update(filename, 'utf8'); 37 | 38 | tmpFileName = (new Date()).getTime() + md5.digest('hex'); 39 | 40 | filePath = path.join(tmpDir, tmpFileName || 'temp.tmp'); 41 | 42 | file.on('end', function () { 43 | req.files[fieldname] = { 44 | type: mimetype, 45 | encoding: encoding, 46 | name: filename, 47 | path: filePath 48 | }; 49 | }); 50 | 51 | file.on('error', function (error) { 52 | console.log('Error', 'Something went wrong uploading the file', error); 53 | }); 54 | 55 | stream = fs.createWriteStream(filePath); 56 | 57 | stream.on('error', function (error) { 58 | console.log('Error', 'Something went wrong uploading the file', error); 59 | }); 60 | 61 | file.pipe(stream); 62 | }); 63 | 64 | busboy.on('error', function (error) { 65 | console.log('Error', 'Something went wrong parsing the form', error); 66 | res.status(500).send({code: 500, message: 'Could not parse upload completely.'}); 67 | }); 68 | 69 | busboy.on('field', function (fieldname, val) { 70 | req.body[fieldname] = val; 71 | }); 72 | 73 | busboy.on('finish', function () { 74 | next(); 75 | }); 76 | 77 | req.pipe(busboy); 78 | } 79 | 80 | module.exports = ghostBusBoy; 81 | -------------------------------------------------------------------------------- /core/server/middleware/auth-strategies.js: -------------------------------------------------------------------------------- 1 | var passport = require('passport'), 2 | BearerStrategy = require('passport-http-bearer').Strategy, 3 | ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy, 4 | models = require('../models'); 5 | 6 | /** 7 | * ClientPasswordStrategy 8 | * 9 | * This strategy is used to authenticate registered OAuth clients. It is 10 | * employed to protect the `token` endpoint, which consumers use to obtain 11 | * access tokens. The OAuth 2.0 specification suggests that clients use the 12 | * HTTP Basic scheme to authenticate (not implemented yet). 13 | 14 | * Use of the client password strategy is implemented to support ember-simple-auth. 15 | */ 16 | passport.use(new ClientPasswordStrategy( 17 | function (clientId, clientSecret, done) { 18 | models.Client.forge({slug: clientId}) 19 | .fetch() 20 | .then(function (model) { 21 | if (model) { 22 | var client = model.toJSON(); 23 | if (client.secret === clientSecret) { 24 | return done(null, client); 25 | } 26 | } 27 | return done(null, false); 28 | }); 29 | } 30 | )); 31 | 32 | /** 33 | * BearerStrategy 34 | * 35 | * This strategy is used to authenticate users based on an access token (aka a 36 | * bearer token). The user must have previously authorized a client 37 | * application, which is issued an access token to make requests on behalf of 38 | * the authorizing user. 39 | */ 40 | passport.use(new BearerStrategy( 41 | function (accessToken, done) { 42 | models.Accesstoken.forge({token: accessToken}) 43 | .fetch() 44 | .then(function (model) { 45 | if (model) { 46 | var token = model.toJSON(); 47 | if (token.expires > Date.now()) { 48 | models.User.forge({id: token.user_id}) 49 | .fetch() 50 | .then(function (model) { 51 | if (model) { 52 | var user = model.toJSON(), 53 | info = {scope: '*'}; 54 | return done(null, {id: user.id}, info); 55 | } 56 | return done(null, false); 57 | }); 58 | } else { 59 | return done(null, false); 60 | } 61 | } else { 62 | return done(null, false); 63 | } 64 | }); 65 | } 66 | )); 67 | -------------------------------------------------------------------------------- /core/server/helpers/foreach.js: -------------------------------------------------------------------------------- 1 | // # Foreach Helper 2 | // Usage: `{{#foreach data}}{{/foreach}}` 3 | // 4 | // Block helper designed for looping through posts 5 | 6 | var hbs = require('express-hbs'), 7 | foreach; 8 | 9 | foreach = function (context, options) { 10 | var fn = options.fn, 11 | inverse = options.inverse, 12 | i = 0, 13 | j = 0, 14 | columns = options.hash.columns, 15 | key, 16 | ret = '', 17 | data; 18 | 19 | if (options.data) { 20 | data = hbs.handlebars.createFrame(options.data); 21 | } 22 | 23 | function setKeys(_data, _i, _j, _columns) { 24 | if (_i === 0) { 25 | _data.first = true; 26 | } 27 | if (_i === _j - 1) { 28 | _data.last = true; 29 | } 30 | // first post is index zero but still needs to be odd 31 | if (_i % 2 === 1) { 32 | _data.even = true; 33 | } else { 34 | _data.odd = true; 35 | } 36 | if (_i % _columns === 0) { 37 | _data.rowStart = true; 38 | } else if (_i % _columns === (_columns - 1)) { 39 | _data.rowEnd = true; 40 | } 41 | return _data; 42 | } 43 | if (context && typeof context === 'object') { 44 | if (context instanceof Array) { 45 | for (j = context.length; i < j; i += 1) { 46 | if (data) { 47 | data.index = i; 48 | data.first = data.rowEnd = data.rowStart = data.last = data.even = data.odd = false; 49 | data = setKeys(data, i, j, columns); 50 | } 51 | ret = ret + fn(context[i], {data: data}); 52 | } 53 | } else { 54 | for (key in context) { 55 | if (context.hasOwnProperty(key)) { 56 | j += 1; 57 | } 58 | } 59 | for (key in context) { 60 | if (context.hasOwnProperty(key)) { 61 | if (data) { 62 | data.key = key; 63 | data.first = data.rowEnd = data.rowStart = data.last = data.even = data.odd = false; 64 | data = setKeys(data, i, j, columns); 65 | } 66 | ret = ret + fn(context[key], {data: data}); 67 | i += 1; 68 | } 69 | } 70 | } 71 | } 72 | 73 | if (i === 0) { 74 | ret = inverse(this); 75 | } 76 | 77 | return ret; 78 | }; 79 | 80 | module.exports = foreach; 81 | -------------------------------------------------------------------------------- /core/server/routes/frontend.js: -------------------------------------------------------------------------------- 1 | var frontend = require('../controllers/frontend'), 2 | config = require('../config'), 3 | express = require('express'), 4 | utils = require('../utils'), 5 | 6 | frontendRoutes; 7 | 8 | frontendRoutes = function () { 9 | var router = express.Router(), 10 | subdir = config.paths.subdir; 11 | 12 | // ### www redirect 13 | router.get('/*', function (req, res, next) { 14 | if (req.headers.host.match(/^www/) !== null) { 15 | res.redirect(301, 'http://' + req.headers.host.replace(/^www\./, '') + req.url); 16 | } else { 17 | next(); 18 | } 19 | }); 20 | 21 | // ### Admin routes 22 | router.get(/^\/(logout|signout)\/$/, function redirect(req, res) { 23 | /*jslint unparam:true*/ 24 | res.set({'Cache-Control': 'public, max-age=' + utils.ONE_YEAR_S}); 25 | res.redirect(301, subdir + '/ghost/signout/'); 26 | }); 27 | router.get(/^\/signup\/$/, function redirect(req, res) { 28 | /*jslint unparam:true*/ 29 | res.set({'Cache-Control': 'public, max-age=' + utils.ONE_YEAR_S}); 30 | res.redirect(301, subdir + '/ghost/signup/'); 31 | }); 32 | 33 | // redirect to /ghost and let that do the authentication to prevent redirects to /ghost//admin etc. 34 | router.get(/^\/((ghost-admin|admin|wp-admin|dashboard|signin|login)\/?)$/, function (req, res) { 35 | /*jslint unparam:true*/ 36 | res.redirect(subdir + '/ghost/'); 37 | }); 38 | 39 | // ### Frontend routes 40 | router.get('/rss/', frontend.rss); 41 | router.get('/rss/:page/', frontend.rss); 42 | router.get('/feed/', function redirect(req, res) { 43 | /*jshint unused:true*/ 44 | res.set({'Cache-Control': 'public, max-age=' + utils.ONE_YEAR_S}); 45 | res.redirect(301, subdir + '/rss/'); 46 | }); 47 | 48 | // Tags 49 | router.get('/tag/:slug/rss/', frontend.rss); 50 | router.get('/tag/:slug/rss/:page/', frontend.rss); 51 | router.get('/tag/:slug/page/:page/', frontend.tag); 52 | router.get('/tag/:slug/', frontend.tag); 53 | 54 | // Authors 55 | router.get('/author/:slug/rss/', frontend.rss); 56 | router.get('/author/:slug/rss/:page/', frontend.rss); 57 | router.get('/author/:slug/page/:page/', frontend.author); 58 | router.get('/author/:slug/', frontend.author); 59 | 60 | // Default 61 | router.get('/page/:page/', frontend.homepage); 62 | router.get('/', frontend.homepage); 63 | router.get('*', frontend.single); 64 | 65 | return router; 66 | }; 67 | 68 | module.exports = frontendRoutes; 69 | -------------------------------------------------------------------------------- /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 | 22 | 23 |
24 |
25 |
26 |
27 |
28 | 32 |
33 |
34 |

{{code}}

35 |

{{message}}

36 | Go to the front page → 37 |
38 |
39 |
40 | {{#if stack}} 41 |
42 |

Stack Trace

43 |

{{message}}

44 |
    45 | {{#each stack}} 46 |
  • 47 | at 48 | {{#if function}}{{function}}{{/if}} 49 | ({{at}}) 50 |
  • 51 | {{/each}} 52 |
53 |
54 | {{/if}} 55 |
56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /content/themes/kevgriffin/post.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 | 3 | {{! The comment above "< default" means - insert everything in this file into 4 | the {body} of the default.hbs template, which contains our header/footer. }} 5 | 6 |
7 | 8 |
9 | 10 | {{! Each post has the blog logo at the top, with a link back to the home page }} 11 |
12 | 19 |
20 | 21 | {{! Everything inside the #post tags pulls data from the post }} 22 | {{#post}} 23 | 24 | 25 | 26 |

{{{title}}}

27 | 28 |
29 | {{content}} 30 |
31 | 32 | 56 | 57 | {{/post}} 58 | 59 |
60 | 61 |
-------------------------------------------------------------------------------- /core/server/api/roles.js: -------------------------------------------------------------------------------- 1 | // # Roles API 2 | // RESTful API for the Role resource 3 | var Promise = require('bluebird'), 4 | _ = require('lodash'), 5 | canThis = require('../permissions').canThis, 6 | dataProvider = require('../models'), 7 | errors = require('../errors'), 8 | 9 | roles; 10 | 11 | /** 12 | * ## Roles API Methods 13 | * 14 | * **See:** [API Methods](index.js.html#api%20methods) 15 | */ 16 | roles = { 17 | /** 18 | * ### Browse 19 | * Find all roles 20 | * 21 | * If a 'permissions' property is passed in the options object then 22 | * the results will be filtered based on whether or not the context user has the given 23 | * permission on a role. 24 | * 25 | * 26 | * @public 27 | * @param {{context, permissions}} options (optional) 28 | * @returns {Promise(Roles)} Roles Collection 29 | */ 30 | browse: function browse(options) { 31 | var permissionMap = []; 32 | options = options || {}; 33 | 34 | return canThis(options.context).browse.role().then(function () { 35 | return dataProvider.Role.findAll(options).then(function (foundRoles) { 36 | if (options.permissions === 'assign') { 37 | // Hacky implementation of filtering because when.filter is only available in when 3.4.0, 38 | // but that's buggy and kills other tests and introduces Heisenbugs. Until we turn everything 39 | // to Bluebird, this works. Sorry. 40 | // TODO: replace with better filter when bluebird lands 41 | _.each(foundRoles.toJSON(), function (role) { 42 | permissionMap.push(canThis(options.context).assign.role(role).then(function () { 43 | if (role.name === 'Owner') { 44 | return null; 45 | } 46 | return role; 47 | }).catch(function () { 48 | return null; 49 | })); 50 | }); 51 | 52 | return Promise.all(permissionMap).then(function (resolved) { 53 | return {roles: _.filter(resolved, function (role) { 54 | return role !== null; 55 | })}; 56 | }).catch(errors.logAndThrowError); 57 | } 58 | return {roles: foundRoles.toJSON()}; 59 | }); 60 | }) 61 | .catch(errors.logAndThrowError); 62 | } 63 | }; 64 | 65 | module.exports = roles; 66 | -------------------------------------------------------------------------------- /core/server/data/versioning/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | errors = require('../../errors'), 3 | config = require('../../config'), 4 | 5 | defaultSettings = require('../default-settings'), 6 | 7 | initialVersion = '000', 8 | defaultDatabaseVersion; 9 | 10 | // Default Database Version 11 | // The migration version number according to the hardcoded default settings 12 | // This is the version the database should be at or migrated to 13 | function getDefaultDatabaseVersion() { 14 | if (!defaultDatabaseVersion) { 15 | // This be the current version according to the software 16 | defaultDatabaseVersion = defaultSettings.core.databaseVersion.defaultValue; 17 | } 18 | 19 | return defaultDatabaseVersion; 20 | } 21 | 22 | // Database Current Version 23 | // The migration version number according to the database 24 | // This is what the database is currently at and may need to be updated 25 | function getDatabaseVersion() { 26 | var knex = config.database.knex; 27 | 28 | return knex.schema.hasTable('settings').then(function (exists) { 29 | // Check for the current version from the settings table 30 | if (exists) { 31 | // Temporary code to deal with old databases with currentVersion settings 32 | return knex('settings') 33 | .where('key', 'databaseVersion') 34 | .orWhere('key', 'currentVersion') 35 | .select('value') 36 | .then(function (versions) { 37 | var databaseVersion = _.reduce(versions, function (memo, version) { 38 | if (isNaN(version.value)) { 39 | errors.throwError('Database version is not recognised'); 40 | } 41 | return parseInt(version.value, 10) > parseInt(memo, 10) ? version.value : memo; 42 | }, initialVersion); 43 | 44 | if (!databaseVersion || databaseVersion.length === 0) { 45 | // we didn't get a response we understood, assume initialVersion 46 | databaseVersion = initialVersion; 47 | } 48 | 49 | return databaseVersion; 50 | }); 51 | } 52 | throw new Error('Settings table does not exist'); 53 | }); 54 | } 55 | 56 | function setDatabaseVersion() { 57 | return config.database.knex('settings') 58 | .where('key', 'databaseVersion') 59 | .update({value: defaultDatabaseVersion}); 60 | } 61 | 62 | module.exports = { 63 | getDefaultDatabaseVersion: getDefaultDatabaseVersion, 64 | getDatabaseVersion: getDatabaseVersion, 65 | setDatabaseVersion: setDatabaseVersion 66 | }; 67 | -------------------------------------------------------------------------------- /core/server/xmlrpc.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | http = require('http'), 3 | xml = require('xml'), 4 | api = require('./api'), 5 | config = require('./config'), 6 | errors = require('./errors'), 7 | pingList; 8 | 9 | // ToDo: Make this configurable 10 | pingList = [{ 11 | host: 'blogsearch.google.com', 12 | path: '/ping/RPC2' 13 | }, { 14 | host: 'rpc.pingomatic.com', 15 | path: '/' 16 | }]; 17 | 18 | function ping(post) { 19 | var pingXML, 20 | title = post.title; 21 | 22 | // Only ping when in production and not a page 23 | if (process.env.NODE_ENV !== 'production' || post.page || config.isPrivacyDisabled('useRpcPing')) { 24 | return; 25 | } 26 | 27 | // Don't ping for the welcome to ghost post. 28 | // This also handles the case where during ghost's first run 29 | // models.init() inserts this post but permissions.init() hasn't 30 | // (can't) run yet. 31 | if (post.slug === 'welcome-to-ghost') { 32 | return; 33 | } 34 | 35 | // Need to require here because of circular dependency 36 | return config.urlForPost(api.settings, post, true).then(function (url) { 37 | // Build XML object. 38 | pingXML = xml({ 39 | methodCall: [{ 40 | methodName: 'weblogUpdate.ping' 41 | }, { 42 | params: [{ 43 | param: [{ 44 | value: [{ 45 | string: title 46 | }] 47 | }] 48 | }, { 49 | param: [{ 50 | value: [{ 51 | string: url 52 | }] 53 | }] 54 | }] 55 | }] 56 | }, {declaration: true}); 57 | 58 | // Ping each of the defined services. 59 | _.each(pingList, function (pingHost) { 60 | var options = { 61 | hostname: pingHost.host, 62 | path: pingHost.path, 63 | method: 'POST' 64 | }, 65 | req; 66 | 67 | req = http.request(options); 68 | req.write(pingXML); 69 | req.on('error', function (error) { 70 | errors.logError( 71 | error, 72 | 'Pinging services for updates on your blog failed, your blog will continue to function.', 73 | 'If you get this error repeatedly, please seek help from https://ghost.org/forum.' 74 | ); 75 | }); 76 | req.end(); 77 | }); 78 | }); 79 | } 80 | 81 | module.exports = { 82 | ping: ping 83 | }; 84 | -------------------------------------------------------------------------------- /core/server/data/migration/commands.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | errors = require('../../errors'), 3 | utils = require('../utils'), 4 | schema = require('../schema').tables, 5 | 6 | // private 7 | logInfo, 8 | 9 | // public 10 | getDeleteCommands, 11 | getAddCommands, 12 | addColumnCommands, 13 | modifyUniqueCommands; 14 | 15 | logInfo = function logInfo(message) { 16 | errors.logInfo('Migrations', message); 17 | }; 18 | 19 | getDeleteCommands = function getDeleteCommands(oldTables, newTables) { 20 | var deleteTables = _.difference(oldTables, newTables); 21 | return _.map(deleteTables, function (table) { 22 | return function () { 23 | logInfo('Deleting table: ' + table); 24 | return utils.deleteTable(table); 25 | }; 26 | }); 27 | }; 28 | getAddCommands = function getAddCommands(oldTables, newTables) { 29 | var addTables = _.difference(newTables, oldTables); 30 | return _.map(addTables, function (table) { 31 | return function () { 32 | logInfo('Creating table: ' + table); 33 | return utils.createTable(table); 34 | }; 35 | }); 36 | }; 37 | addColumnCommands = function addColumnCommands(table, columns) { 38 | var columnKeys = _.keys(schema[table]), 39 | addColumns = _.difference(columnKeys, columns); 40 | 41 | return _.map(addColumns, function (column) { 42 | return function () { 43 | logInfo('Adding column: ' + table + '.' + column); 44 | return utils.addColumn(table, column); 45 | }; 46 | }); 47 | }; 48 | modifyUniqueCommands = function modifyUniqueCommands(table, indexes) { 49 | var columnKeys = _.keys(schema[table]); 50 | return _.map(columnKeys, function (column) { 51 | if (schema[table][column].unique && schema[table][column].unique === true) { 52 | if (!_.contains(indexes, table + '_' + column + '_unique')) { 53 | return function () { 54 | logInfo('Adding unique on: ' + table + '.' + column); 55 | return utils.addUnique(table, column); 56 | }; 57 | } 58 | } else if (!schema[table][column].unique) { 59 | if (_.contains(indexes, table + '_' + column + '_unique')) { 60 | return function () { 61 | logInfo('Dropping unique on: ' + table + '.' + column); 62 | return utils.dropUnique(table, column); 63 | }; 64 | } 65 | } 66 | }); 67 | }; 68 | 69 | module.exports = { 70 | getDeleteCommands: getDeleteCommands, 71 | getAddCommands: getAddCommands, 72 | addColumnCommands: addColumnCommands, 73 | modifyUniqueCommands: modifyUniqueCommands 74 | }; 75 | -------------------------------------------------------------------------------- /core/server/models/basetoken.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | ghostBookshelf = require('./base'), 3 | errors = require('../errors'), 4 | 5 | Basetoken; 6 | 7 | Basetoken = ghostBookshelf.Model.extend({ 8 | 9 | user: function () { 10 | return this.belongsTo('User'); 11 | }, 12 | 13 | client: function () { 14 | return this.belongsTo('Client'); 15 | }, 16 | 17 | // override for base function since we don't have 18 | // a created_by field for sessions 19 | creating: function (newObj, attr, options) { 20 | /*jshint unused:false*/ 21 | }, 22 | 23 | // override for base function since we don't have 24 | // a updated_by field for sessions 25 | saving: function (newObj, attr, options) { 26 | /*jshint unused:false*/ 27 | // Remove any properties which don't belong on the model 28 | this.attributes = this.pick(this.permittedAttributes()); 29 | } 30 | 31 | }, { 32 | destroyAllExpired: function (options) { 33 | options = this.filterOptions(options, 'destroyAll'); 34 | return ghostBookshelf.Collection.forge([], {model: this}) 35 | .query('where', 'expires', '<', Date.now()) 36 | .fetch(options) 37 | .then(function (collection) { 38 | collection.invokeThen('destroy', options); 39 | }); 40 | }, 41 | /** 42 | * ### destroyByUser 43 | * @param {[type]} options has context and id. Context is the user doing the destroy, id is the user to destroy 44 | */ 45 | destroyByUser: function (options) { 46 | var userId = options.id; 47 | 48 | options = this.filterOptions(options, 'destroyByUser'); 49 | 50 | if (userId) { 51 | return ghostBookshelf.Collection.forge([], {model: this}) 52 | .query('where', 'user_id', '=', userId) 53 | .fetch(options) 54 | .then(function (collection) { 55 | collection.invokeThen('destroy', options); 56 | }); 57 | } 58 | 59 | return Promise.reject(new errors.NotFoundError('No user found')); 60 | }, 61 | 62 | /** 63 | * ### destroyByToken 64 | * @param {[type]} options has token where token is the token to destroy 65 | */ 66 | destroyByToken: function (options) { 67 | var token = options.token; 68 | 69 | options = this.filterOptions(options, 'destroyByUser'); 70 | 71 | if (token) { 72 | return ghostBookshelf.Collection.forge([], {model: this}) 73 | .query('where', 'token', '=', token) 74 | .fetch(options) 75 | .then(function (collection) { 76 | collection.invokeThen('destroy', options); 77 | }); 78 | } 79 | 80 | return Promise.reject(new errors.NotFoundError('Token not found')); 81 | } 82 | }); 83 | 84 | module.exports = Basetoken; 85 | -------------------------------------------------------------------------------- /core/shared/lib/showdown/extensions/ghostimagepreview.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, browser:true */ 2 | /* global Ember */ 3 | 4 | // Ghost Image Preview 5 | // 6 | // Manages the conversion of image markdown `![]()` from markdown into the HTML image preview 7 | // This provides a dropzone and other interface elements for adding images 8 | // Is only used in the admin client. 9 | 10 | var Ghost = Ghost || {}; 11 | (function () { 12 | var ghostimagepreview = function () { 13 | return [ 14 | // ![] image syntax 15 | { 16 | type: 'lang', 17 | filter: function (text) { 18 | var imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim, 19 | /* regex from isURL in node-validator. Yum! */ 20 | 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, 21 | pathRegex = /^(\/)?([^\/\0]+(\/)?)+$/i; 22 | 23 | return text.replace(imageMarkdownRegex, function (match, key, alt, src) { 24 | var result = '', 25 | output; 26 | 27 | if (src && (src.match(uriRegex) || src.match(pathRegex))) { 28 | result = ''; 29 | } 30 | 31 | if ((Ghost && Ghost.touchEditor) || (typeof window !== 'undefined' && Ember.touchEditor)) { 32 | output = '
' + 33 | result + '
Mobile uploads coming soon
'; 34 | } else { 35 | output = '
' + 36 | result + '
Add image of ' + alt + '
' + 37 | '' + 38 | '
'; 39 | } 40 | 41 | return output; 42 | }); 43 | } 44 | } 45 | ]; 46 | }; 47 | 48 | // Client-side export 49 | if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) { 50 | window.Showdown.extensions.ghostimagepreview = ghostimagepreview; 51 | } 52 | // Server-side export 53 | if (typeof module !== 'undefined') { 54 | module.exports = ghostimagepreview; 55 | } 56 | }()); 57 | -------------------------------------------------------------------------------- /content/themes/casper/assets/js/jquery.fitvids.js: -------------------------------------------------------------------------------- 1 | /*global jQuery */ 2 | /*jshint browser:true */ 3 | /*! 4 | * FitVids 1.1 5 | * 6 | * Copyright 2013, Chris Coyier - http://css-tricks.com + Dave Rupert - http://daverupert.com 7 | * Credit to Thierry Koblentz - http://www.alistapart.com/articles/creating-intrinsic-ratios-for-video/ 8 | * Released under the WTFPL license - http://sam.zoy.org/wtfpl/ 9 | * 10 | */ 11 | 12 | (function( $ ){ 13 | 14 | "use strict"; 15 | 16 | $.fn.fitVids = function( options ) { 17 | var settings = { 18 | customSelector: null 19 | }; 20 | 21 | if(!document.getElementById('fit-vids-style')) { 22 | // appendStyles: https://github.com/toddmotto/fluidvids/blob/master/dist/fluidvids.js 23 | var head = document.head || document.getElementsByTagName('head')[0]; 24 | var css = '.fluid-width-video-wrapper{width:100%;position:relative;padding:0;}.fluid-width-video-wrapper iframe,.fluid-width-video-wrapper object,.fluid-width-video-wrapper embed {position:absolute;top:0;left:0;width:100%;height:100%;}'; 25 | var div = document.createElement('div'); 26 | div.innerHTML = '

x

'; 27 | head.appendChild(div.childNodes[1]); 28 | } 29 | 30 | if ( options ) { 31 | $.extend( settings, options ); 32 | } 33 | 34 | return this.each(function(){ 35 | var selectors = [ 36 | "iframe[src*='player.vimeo.com']", 37 | "iframe[src*='youtube.com']", 38 | "iframe[src*='youtube-nocookie.com']", 39 | "iframe[src*='kickstarter.com'][src*='video.html']", 40 | "object", 41 | "embed" 42 | ]; 43 | 44 | if (settings.customSelector) { 45 | selectors.push(settings.customSelector); 46 | } 47 | 48 | var $allVideos = $(this).find(selectors.join(',')); 49 | $allVideos = $allVideos.not("object object"); // SwfObj conflict patch 50 | 51 | $allVideos.each(function(){ 52 | var $this = $(this); 53 | if (this.tagName.toLowerCase() === 'embed' && $this.parent('object').length || $this.parent('.fluid-width-video-wrapper').length) { return; } 54 | var height = ( this.tagName.toLowerCase() === 'object' || ($this.attr('height') && !isNaN(parseInt($this.attr('height'), 10))) ) ? parseInt($this.attr('height'), 10) : $this.height(), 55 | width = !isNaN(parseInt($this.attr('width'), 10)) ? parseInt($this.attr('width'), 10) : $this.width(), 56 | aspectRatio = height / width; 57 | if(!$this.attr('id')){ 58 | var videoID = 'fitvid' + Math.floor(Math.random()*999999); 59 | $this.attr('id', videoID); 60 | } 61 | $this.wrap('
').parent('.fluid-width-video-wrapper').css('padding-top', (aspectRatio * 100)+"%"); 62 | $this.removeAttr('height').removeAttr('width'); 63 | }); 64 | }); 65 | }; 66 | // Works with either jQuery or Zepto 67 | })( window.jQuery || window.Zepto ); 68 | -------------------------------------------------------------------------------- /content/themes/kevgriffin/assets/js/jquery.fitvids.js: -------------------------------------------------------------------------------- 1 | /*global jQuery */ 2 | /*jshint multistr:true browser:true */ 3 | /*! 4 | * FitVids 1.0.3 5 | * 6 | * Copyright 2013, Chris Coyier - http://css-tricks.com + Dave Rupert - http://daverupert.com 7 | * Credit to Thierry Koblentz - http://www.alistapart.com/articles/creating-intrinsic-ratios-for-video/ 8 | * Released under the WTFPL license - http://sam.zoy.org/wtfpl/ 9 | * 10 | * Date: Thu Sept 01 18:00:00 2011 -0500 11 | */ 12 | 13 | (function( $ ){ 14 | 15 | "use strict"; 16 | 17 | $.fn.fitVids = function( options ) { 18 | var settings = { 19 | customSelector: null 20 | }; 21 | 22 | if(!document.getElementById('fit-vids-style')) { 23 | 24 | var div = document.createElement('div'), 25 | ref = document.getElementsByTagName('base')[0] || document.getElementsByTagName('script')[0], 26 | cssStyles = '­'; 27 | 28 | div.className = 'fit-vids-style'; 29 | div.id = 'fit-vids-style'; 30 | div.style.display = 'none'; 31 | div.innerHTML = cssStyles; 32 | 33 | ref.parentNode.insertBefore(div,ref); 34 | 35 | } 36 | 37 | if ( options ) { 38 | $.extend( settings, options ); 39 | } 40 | 41 | return this.each(function(){ 42 | var selectors = [ 43 | "iframe[src*='player.vimeo.com']", 44 | "iframe[src*='youtube.com']", 45 | "iframe[src*='youtube-nocookie.com']", 46 | "iframe[src*='kickstarter.com'][src*='video.html']", 47 | "object", 48 | "embed" 49 | ]; 50 | 51 | if (settings.customSelector) { 52 | selectors.push(settings.customSelector); 53 | } 54 | 55 | var $allVideos = $(this).find(selectors.join(',')); 56 | $allVideos = $allVideos.not("object object"); // SwfObj conflict patch 57 | 58 | $allVideos.each(function(){ 59 | var $this = $(this); 60 | if (this.tagName.toLowerCase() === 'embed' && $this.parent('object').length || $this.parent('.fluid-width-video-wrapper').length) { return; } 61 | var height = ( this.tagName.toLowerCase() === 'object' || ($this.attr('height') && !isNaN(parseInt($this.attr('height'), 10))) ) ? parseInt($this.attr('height'), 10) : $this.height(), 62 | width = !isNaN(parseInt($this.attr('width'), 10)) ? parseInt($this.attr('width'), 10) : $this.width(), 63 | aspectRatio = height / width; 64 | if(!$this.attr('id')){ 65 | var videoID = 'fitvid' + Math.floor(Math.random()*999999); 66 | $this.attr('id', videoID); 67 | } 68 | $this.wrap('
').parent('.fluid-width-video-wrapper').css('padding-top', (aspectRatio * 100)+"%"); 69 | $this.removeAttr('height').removeAttr('width'); 70 | }); 71 | }); 72 | }; 73 | // Works with either jQuery or Zepto 74 | })( window.jQuery || window.Zepto ); 75 | -------------------------------------------------------------------------------- /core/server/helpers/body_class.js: -------------------------------------------------------------------------------- 1 | // # Body Class Helper 2 | // Usage: `{{body_class}}` 3 | // 4 | // Output classes for the body element 5 | // 6 | // We use the name body_class to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var hbs = require('express-hbs'), 10 | _ = require('lodash'), 11 | api = require('../api'), 12 | config = require('../config'), 13 | filters = require('../filters'), 14 | template = require('./template'), 15 | body_class; 16 | 17 | body_class = function () { 18 | var classes = [], 19 | post = this.post, 20 | tags = this.post && this.post.tags ? this.post.tags : this.tags || [], 21 | page = this.post && this.post.page ? this.post.page : this.page || false; 22 | 23 | if (this.tag !== undefined) { 24 | classes.push('tag-template'); 25 | classes.push('tag-' + this.tag.slug); 26 | } 27 | 28 | if (this.author !== undefined) { 29 | classes.push('author-template'); 30 | classes.push('author-' + this.author.slug); 31 | } 32 | 33 | if (_.isString(this.relativeUrl) && this.relativeUrl.match(/\/(page\/\d)/)) { 34 | classes.push('paged'); 35 | // To be removed from pages by #2597 when we're ready to deprecate this 36 | classes.push('archive-template'); 37 | } else if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '') { 38 | classes.push('home-template'); 39 | } else if (post) { 40 | // To be removed from pages by #2597 when we're ready to deprecate this 41 | // i.e. this should be if (post && !page) { ... } 42 | classes.push('post-template'); 43 | } 44 | 45 | if (page) { 46 | classes.push('page-template'); 47 | // To be removed by #2597 when we're ready to deprecate this 48 | classes.push('page'); 49 | } 50 | 51 | if (tags) { 52 | classes = classes.concat(tags.map(function (tag) { return 'tag-' + tag.slug; })); 53 | } 54 | 55 | return api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function (response) { 56 | var activeTheme = response.settings[0], 57 | paths = config.paths.availableThemes[activeTheme.value], 58 | view; 59 | 60 | if (post && page) { 61 | view = template.getThemeViewForPost(paths, post).split('-'); 62 | 63 | if (view[0] === 'page' && view.length > 1) { 64 | classes.push(view.join('-')); 65 | // To be removed by #2597 when we're ready to deprecate this 66 | view.splice(1, 0, 'template'); 67 | classes.push(view.join('-')); 68 | } 69 | } 70 | 71 | return filters.doFilter('body_class', classes).then(function (classes) { 72 | var classString = _.reduce(classes, function (memo, item) { return memo + ' ' + item; }, ''); 73 | return new hbs.handlebars.SafeString(classString.trim()); 74 | }); 75 | }); 76 | }; 77 | 78 | module.exports = body_class; 79 | -------------------------------------------------------------------------------- /core/server/apps/sandbox.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require('path'), 3 | Module = require('module'), 4 | _ = require('lodash'); 5 | 6 | function AppSandbox(opts) { 7 | this.opts = _.defaults(opts || {}, AppSandbox.defaults); 8 | } 9 | 10 | AppSandbox.prototype.loadApp = function loadAppSandboxed(appPath) { 11 | var appFile = require.resolve(appPath), 12 | appBase = path.dirname(appFile); 13 | 14 | this.opts.appRoot = appBase; 15 | 16 | return this.loadModule(appPath); 17 | }; 18 | 19 | AppSandbox.prototype.loadModule = function loadModuleSandboxed(modulePath) { 20 | // Set loaded modules parent to this 21 | var self = this, 22 | moduleDir = path.dirname(modulePath), 23 | parentModulePath = self.opts.parent || module.parent, 24 | appRoot = self.opts.appRoot || moduleDir, 25 | currentModule, 26 | nodeRequire; 27 | 28 | // Resolve the modules path 29 | modulePath = Module._resolveFilename(modulePath, parentModulePath); 30 | 31 | // Instantiate a Node Module class 32 | currentModule = new Module(modulePath, parentModulePath); 33 | 34 | // Grab the original modules require function 35 | nodeRequire = currentModule.require; 36 | 37 | // Set a new proxy require function 38 | currentModule.require = function requireProxy(module) { 39 | // check whitelist, plugin config, etc. 40 | if (_.contains(self.opts.blacklist, module)) { 41 | throw new Error('Unsafe App require: ' + module); 42 | } 43 | 44 | var firstTwo = module.slice(0, 2), 45 | resolvedPath, 46 | relPath, 47 | innerBox, 48 | newOpts; 49 | 50 | // Load relative modules with their own sandbox 51 | if (firstTwo === './' || firstTwo === '..') { 52 | // Get the path relative to the modules directory 53 | resolvedPath = path.resolve(moduleDir, module); 54 | 55 | // Check relative path from the appRoot for outside requires 56 | relPath = path.relative(appRoot, resolvedPath); 57 | if (relPath.slice(0, 2) === '..') { 58 | throw new Error('Unsafe App require: ' + relPath); 59 | } 60 | 61 | // Assign as new module path 62 | module = resolvedPath; 63 | 64 | // Pass down the same options 65 | newOpts = _.extend({}, self.opts); 66 | 67 | // Make sure the appRoot and parent are appropriate 68 | newOpts.appRoot = appRoot; 69 | newOpts.parent = currentModule.parent; 70 | 71 | // Create the inner sandbox for loading this module. 72 | innerBox = new AppSandbox(newOpts); 73 | 74 | return innerBox.loadModule(module); 75 | } 76 | 77 | // Call the original require method for white listed named modules 78 | return nodeRequire.call(currentModule, module); 79 | }; 80 | 81 | currentModule.load(currentModule.id); 82 | 83 | return currentModule.exports; 84 | }; 85 | 86 | AppSandbox.defaults = { 87 | blacklist: ['knex', 'fs', 'http', 'sqlite3', 'pg', 'mysql', 'ghost'] 88 | }; 89 | 90 | module.exports = AppSandbox; 91 | -------------------------------------------------------------------------------- /core/server/filters.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | pipeline = require('./utils/pipeline'), 3 | _ = require('lodash'), 4 | defaults; 5 | 6 | // ## Default values 7 | /** 8 | * A hash of default values to use instead of 'magic' numbers/strings. 9 | * @type {Object} 10 | */ 11 | defaults = { 12 | filterPriority: 5, 13 | maxPriority: 9 14 | }; 15 | 16 | function Filters() { 17 | // Holds the filters 18 | this.filterCallbacks = []; 19 | 20 | // Holds the filter hooks (that are built in to Ghost Core) 21 | this.filters = []; 22 | } 23 | 24 | // Register a new filter callback function 25 | Filters.prototype.registerFilter = function (name, priority, fn) { 26 | // Carry the priority optional parameter to a default of 5 27 | if (_.isFunction(priority)) { 28 | fn = priority; 29 | priority = null; 30 | } 31 | 32 | // Null priority should be set to default 33 | if (priority === null) { 34 | priority = defaults.filterPriority; 35 | } 36 | 37 | this.filterCallbacks[name] = this.filterCallbacks[name] || {}; 38 | this.filterCallbacks[name][priority] = this.filterCallbacks[name][priority] || []; 39 | 40 | this.filterCallbacks[name][priority].push(fn); 41 | }; 42 | 43 | // Unregister a filter callback function 44 | Filters.prototype.deregisterFilter = function (name, priority, fn) { 45 | // Curry the priority optional parameter to a default of 5 46 | if (_.isFunction(priority)) { 47 | fn = priority; 48 | priority = defaults.filterPriority; 49 | } 50 | 51 | // Check if it even exists 52 | if (this.filterCallbacks[name] && this.filterCallbacks[name][priority]) { 53 | // Remove the function from the list of filter funcs 54 | this.filterCallbacks[name][priority] = _.without(this.filterCallbacks[name][priority], fn); 55 | } 56 | }; 57 | 58 | // Execute filter functions in priority order 59 | Filters.prototype.doFilter = function (name, args, context) { 60 | var callbacks = this.filterCallbacks[name], 61 | priorityCallbacks = []; 62 | 63 | // Bug out early if no callbacks by that name 64 | if (!callbacks) { 65 | return Promise.resolve(args); 66 | } 67 | 68 | // For each priorityLevel 69 | _.times(defaults.maxPriority + 1, function (priority) { 70 | // Add a function that runs its priority level callbacks in a pipeline 71 | priorityCallbacks.push(function (currentArgs) { 72 | var callables; 73 | 74 | // Bug out if no handlers on this priority 75 | if (!_.isArray(callbacks[priority])) { 76 | return Promise.resolve(currentArgs); 77 | } 78 | 79 | callables = _.map(callbacks[priority], function (callback) { 80 | return function (args) { 81 | return callback(args, context); 82 | }; 83 | }); 84 | // Call each handler for this priority level, allowing for promises or values 85 | return pipeline(callables, currentArgs); 86 | }); 87 | }); 88 | 89 | return pipeline(priorityCallbacks, args); 90 | }; 91 | 92 | module.exports = new Filters(); 93 | module.exports.Filters = Filters; 94 | -------------------------------------------------------------------------------- /core/server/models/role.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | errors = require('../errors'), 3 | ghostBookshelf = require('./base'), 4 | Promise = require('bluebird'), 5 | 6 | Role, 7 | Roles; 8 | 9 | Role = ghostBookshelf.Model.extend({ 10 | 11 | tableName: 'roles', 12 | 13 | users: function () { 14 | return this.belongsToMany('User'); 15 | }, 16 | 17 | permissions: function () { 18 | return this.belongsToMany('Permission'); 19 | } 20 | }, { 21 | /** 22 | * Returns an array of keys permitted in a method's `options` hash, depending on the current method. 23 | * @param {String} methodName The name of the method to check valid options for. 24 | * @return {Array} Keys allowed in the `options` hash of the model's method. 25 | */ 26 | permittedOptions: function (methodName) { 27 | var options = ghostBookshelf.Model.permittedOptions(), 28 | 29 | // whitelists for the `options` hash argument on methods, by method name. 30 | // these are the only options that can be passed to Bookshelf / Knex. 31 | validOptions = { 32 | findOne: ['withRelated'] 33 | }; 34 | 35 | if (validOptions[methodName]) { 36 | options = options.concat(validOptions[methodName]); 37 | } 38 | 39 | return options; 40 | }, 41 | 42 | permissible: function (roleModelOrId, action, context, loadedPermissions, hasUserPermission, hasAppPermission) { 43 | var self = this, 44 | checkAgainst = [], 45 | origArgs; 46 | 47 | // If we passed in an id instead of a model, get the model 48 | // then check the permissions 49 | if (_.isNumber(roleModelOrId) || _.isString(roleModelOrId)) { 50 | // Grab the original args without the first one 51 | origArgs = _.toArray(arguments).slice(1); 52 | // Get the actual post model 53 | return this.findOne({id: roleModelOrId, status: 'all'}).then(function (foundRoleModel) { 54 | // Build up the original args but substitute with actual model 55 | var newArgs = [foundRoleModel].concat(origArgs); 56 | 57 | return self.permissible.apply(self, newArgs); 58 | }, errors.logAndThrowError); 59 | } 60 | 61 | if (action === 'assign' && loadedPermissions.user) { 62 | if (_.any(loadedPermissions.user.roles, {name: 'Owner'})) { 63 | checkAgainst = ['Owner', 'Administrator', 'Editor', 'Author']; 64 | } else if (_.any(loadedPermissions.user.roles, {name: 'Administrator'})) { 65 | checkAgainst = ['Administrator', 'Editor', 'Author']; 66 | } else if (_.any(loadedPermissions.user.roles, {name: 'Editor'})) { 67 | checkAgainst = ['Author']; 68 | } 69 | 70 | // Role in the list of permissible roles 71 | hasUserPermission = roleModelOrId && _.contains(checkAgainst, roleModelOrId.get('name')); 72 | } 73 | 74 | if (hasUserPermission && hasAppPermission) { 75 | return Promise.resolve(); 76 | } 77 | 78 | return Promise.reject(); 79 | } 80 | }); 81 | 82 | Roles = ghostBookshelf.Collection.extend({ 83 | model: Role 84 | }); 85 | 86 | module.exports = { 87 | Role: ghostBookshelf.model('Role', Role), 88 | Roles: ghostBookshelf.collection('Roles', Roles) 89 | }; 90 | -------------------------------------------------------------------------------- /content/themes/casper/assets/js/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main JS file for Casper behaviours 3 | */ 4 | 5 | /* globals jQuery, document */ 6 | (function ($, sr, undefined) { 7 | "use strict"; 8 | 9 | var $document = $(document), 10 | 11 | // debouncing function from John Hann 12 | // http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/ 13 | debounce = function (func, threshold, execAsap) { 14 | var timeout; 15 | 16 | return function debounced () { 17 | var obj = this, args = arguments; 18 | function delayed () { 19 | if (!execAsap) { 20 | func.apply(obj, args); 21 | } 22 | timeout = null; 23 | } 24 | 25 | if (timeout) { 26 | clearTimeout(timeout); 27 | } else if (execAsap) { 28 | func.apply(obj, args); 29 | } 30 | 31 | timeout = setTimeout(delayed, threshold || 100); 32 | }; 33 | }; 34 | 35 | $document.ready(function () { 36 | 37 | var $postContent = $(".post-content"); 38 | $postContent.fitVids(); 39 | 40 | function updateImageWidth() { 41 | var $this = $(this), 42 | contentWidth = $postContent.outerWidth(), // Width of the content 43 | imageWidth = this.naturalWidth; // Original image resolution 44 | 45 | if (imageWidth >= contentWidth) { 46 | $this.addClass('full-img'); 47 | } else { 48 | $this.removeClass('full-img'); 49 | } 50 | } 51 | 52 | var $img = $("img").on('load', updateImageWidth); 53 | function casperFullImg() { 54 | $img.each(updateImageWidth); 55 | } 56 | 57 | casperFullImg(); 58 | $(window).smartresize(casperFullImg); 59 | 60 | $(".scroll-down").arctic_scroll(); 61 | 62 | }); 63 | 64 | // smartresize 65 | jQuery.fn[sr] = function(fn) { return fn ? this.bind('resize', debounce(fn)) : this.trigger(sr); }; 66 | 67 | // Arctic Scroll by Paul Adam Davis 68 | // https://github.com/PaulAdamDavis/Arctic-Scroll 69 | $.fn.arctic_scroll = function (options) { 70 | 71 | var defaults = { 72 | elem: $(this), 73 | speed: 500 74 | }, 75 | 76 | allOptions = $.extend(defaults, options); 77 | 78 | allOptions.elem.click(function (event) { 79 | event.preventDefault(); 80 | var $this = $(this), 81 | $htmlBody = $('html, body'), 82 | offset = ($this.attr('data-offset')) ? $this.attr('data-offset') : false, 83 | position = ($this.attr('data-position')) ? $this.attr('data-position') : false, 84 | toMove; 85 | 86 | if (offset) { 87 | toMove = parseInt(offset); 88 | $htmlBody.stop(true, false).animate({scrollTop: ($(this.hash).offset().top + toMove) }, allOptions.speed); 89 | } else if (position) { 90 | toMove = parseInt(position); 91 | $htmlBody.stop(true, false).animate({scrollTop: toMove }, allOptions.speed); 92 | } else { 93 | $htmlBody.stop(true, false).animate({scrollTop: ($(this.hash).offset().top) }, allOptions.speed); 94 | } 95 | }); 96 | 97 | }; 98 | })(jQuery, 'smartresize'); 99 | -------------------------------------------------------------------------------- /core/server/email-templates/reset-password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 49 | 50 |
13 | 14 | 15 | 16 | 45 | 46 |
17 | 18 |
19 | 20 | 21 | 31 | 32 |
22 | 23 | 24 |

Hello!

25 |

A request has been made to reset your password on {{ siteUrl }}.

26 |

Please follow the link below to reset your password:

Click here to reset your password

27 |

Ghost

28 | 29 | 30 |
33 |
34 |
35 | 36 | 37 | 40 | 41 | 42 |
43 | 44 |
47 | 48 |
51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "ghost", 3 | "version" : "0.5.3", 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 | "licenses" : [ 19 | { 20 | "type": "MIT", 21 | "url": "https://raw.github.com/TryGhost/Ghost/master/LICENSE" 22 | } 23 | ], 24 | "main": "./core/index", 25 | "scripts": { 26 | "start": "node index", 27 | "test": "./node_modules/.bin/grunt validate --verbose" 28 | }, 29 | "engines": { 30 | "node": "~0.10.0" 31 | }, 32 | "engineStrict": true, 33 | "dependencies": { 34 | "bcryptjs": "0.7.10", 35 | "bluebird": "2.3.0", 36 | "body-parser": "1.8.2", 37 | "bookshelf": "0.7.6", 38 | "busboy": "0.2.8", 39 | "cheerio": "0.17.0", 40 | "colors": "0.6.2", 41 | "compression": "1.1.0", 42 | "connect-slashes": "1.2.0", 43 | "downsize": "0.0.5", 44 | "express": "4.9.2", 45 | "express-hbs": "0.7.11", 46 | "fs-extra": "0.8.1", 47 | "html-to-text": "^0.1.0", 48 | "knex": "0.6.21", 49 | "lodash": "2.4.1", 50 | "moment": "2.4.0", 51 | "morgan": "1.3.1", 52 | "node-uuid": "1.4.1", 53 | "nodemailer": "0.7.1", 54 | "oauth2orize": "1.0.1", 55 | "passport": "0.2.0", 56 | "passport-http-bearer": "1.0.1", 57 | "passport-oauth2-client-password": "0.1.1", 58 | "rss": "0.2.1", 59 | "semver": "2.2.1", 60 | "showdown": "https://github.com/ErisDS/showdown/archive/v0.3.2-ghost.tar.gz", 61 | "sqlite3": "2.2.7", 62 | "unidecode": "0.1.3", 63 | "validator": "3.4.0", 64 | "xml": "0.0.12" 65 | }, 66 | "optionalDependencies": { 67 | "mysql": "2.1.1" 68 | }, 69 | "devDependencies": { 70 | "blanket": "~1.1.6", 71 | "bower": "~1.3.10", 72 | "ember-template-compiler": "1.7.0", 73 | "grunt": "~0.4.5", 74 | "grunt-cli": "~0.1.13", 75 | "grunt-autoprefixer": "1.0.1", 76 | "grunt-concat-sourcemap": "~0.4.3", 77 | "grunt-contrib-clean": "~0.6.0", 78 | "grunt-contrib-compress": "~0.11.0", 79 | "grunt-contrib-concat": "~0.5.0", 80 | "grunt-contrib-copy": "~0.5.0", 81 | "grunt-contrib-jshint": "~0.10.0", 82 | "grunt-contrib-uglify": "~0.5.1", 83 | "grunt-contrib-watch": "~0.6.1", 84 | "grunt-docker": "~0.0.8", 85 | "grunt-ember-templates": "~0.4.21", 86 | "grunt-es6-module-transpiler": "~0.6.0", 87 | "grunt-express-server": "~0.4.19", 88 | "grunt-jscs": "~0.7.1", 89 | "grunt-mocha-cli": "~1.10.0", 90 | "grunt-sass": "~0.16.0", 91 | "grunt-shell": "~1.1.1", 92 | "grunt-update-submodules": "~0.4.1", 93 | "matchdep": "~0.3.0", 94 | "mocha": "~1.21.4", 95 | "nock": "0.47.0", 96 | "request": "~2.42.0", 97 | "require-dir": "~0.1.0", 98 | "rewire": "~2.1.0", 99 | "should": "~4.0.4", 100 | "sinon": "~1.10.3", 101 | "supertest": "~0.13.0", 102 | "top-gh-contribs": "0.0.2" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /core/server/apps/proxy.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | api = require('../api'), 3 | helpers = require('../helpers'), 4 | filters = require('../filters'), 5 | generateProxyFunctions; 6 | 7 | generateProxyFunctions = function (name, permissions) { 8 | var getPermission = function (perm) { 9 | return permissions[perm]; 10 | }, 11 | getPermissionToMethod = function (perm, method) { 12 | var perms = getPermission(perm); 13 | 14 | if (!perms) { 15 | return false; 16 | } 17 | 18 | return _.find(perms, function (name) { 19 | return name === method; 20 | }); 21 | }, 22 | runIfPermissionToMethod = function (perm, method, wrappedFunc, context, args) { 23 | var permValue = getPermissionToMethod(perm, method); 24 | 25 | if (!permValue) { 26 | throw new Error('The App "' + name + '" attempted to perform an action or access a resource (' + perm + '.' + method + ') without permission.'); 27 | } 28 | 29 | return wrappedFunc.apply(context, args); 30 | }, 31 | checkRegisterPermissions = function (perm, registerMethod) { 32 | return _.wrap(registerMethod, function (origRegister, name) { 33 | return runIfPermissionToMethod(perm, name, origRegister, this, _.toArray(arguments).slice(1)); 34 | }); 35 | }, 36 | passThruAppContextToApi = function (perm, apiMethods) { 37 | var appContext = { 38 | app: name 39 | }; 40 | 41 | return _.reduce(apiMethods, function (memo, apiMethod, methodName) { 42 | memo[methodName] = function () { 43 | var args = _.toArray(arguments), 44 | options = args[args.length - 1]; 45 | 46 | if (_.isObject(options)) { 47 | options.context = _.clone(appContext); 48 | } 49 | return apiMethod.apply({}, args); 50 | }; 51 | 52 | return memo; 53 | }, {}); 54 | }, 55 | proxy; 56 | 57 | proxy = { 58 | filters: { 59 | register: checkRegisterPermissions('filters', filters.registerFilter.bind(filters)), 60 | deregister: checkRegisterPermissions('filters', filters.deregisterFilter.bind(filters)) 61 | }, 62 | helpers: { 63 | register: checkRegisterPermissions('helpers', helpers.registerThemeHelper.bind(helpers)), 64 | registerAsync: checkRegisterPermissions('helpers', helpers.registerAsyncThemeHelper.bind(helpers)) 65 | }, 66 | api: { 67 | posts: passThruAppContextToApi('posts', 68 | _.pick(api.posts, 'browse', 'read', 'edit', 'add', 'destroy') 69 | ), 70 | tags: passThruAppContextToApi('tags', 71 | _.pick(api.tags, 'browse') 72 | ), 73 | notifications: passThruAppContextToApi('notifications', 74 | _.pick(api.notifications, 'browse', 'add', 'destroy') 75 | ), 76 | settings: passThruAppContextToApi('settings', 77 | _.pick(api.settings, 'browse', 'read', 'edit') 78 | ) 79 | } 80 | }; 81 | 82 | return proxy; 83 | }; 84 | 85 | function AppProxy(options) { 86 | if (!options.name) { 87 | throw new Error('Must provide an app name for api context'); 88 | } 89 | 90 | if (!options.permissions) { 91 | throw new Error('Must provide app permissions'); 92 | } 93 | 94 | _.extend(this, generateProxyFunctions(options.name, options.permissions)); 95 | } 96 | 97 | module.exports = AppProxy; 98 | -------------------------------------------------------------------------------- /core/server/email-templates/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 52 | 53 |
13 | 14 | 15 | 16 | 48 | 49 |
17 | 18 |
19 | 20 | 21 | 34 | 35 |
22 | 23 | 24 |

Hello there!

25 |

Excellent! 26 | You've successfully setup your email config for your Ghost blog over on {{ siteUrl }}

27 |

If you hadn't, you wouldn't be reading this email, but you are, so it looks like all is well :)

28 |

xoxo

29 |

Team Ghost
30 | https://ghost.org

31 | 32 | 33 |
36 |
37 |
38 | 39 | 40 | 43 | 44 | 45 |
46 | 47 |
50 | 51 |
54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /content/themes/casper/assets/fonts/icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /core/server/routes/api.js: -------------------------------------------------------------------------------- 1 | // # API routes 2 | var express = require('express'), 3 | api = require('../api'), 4 | apiRoutes; 5 | 6 | apiRoutes = function (middleware) { 7 | var router = express.Router(); 8 | // alias delete with del 9 | router.del = router.delete; 10 | 11 | // ## Configuration 12 | router.get('/configuration', api.http(api.configuration.browse)); 13 | router.get('/configuration/:key', api.http(api.configuration.read)); 14 | 15 | // ## Posts 16 | router.get('/posts', api.http(api.posts.browse)); 17 | router.post('/posts', api.http(api.posts.add)); 18 | router.get('/posts/:id', api.http(api.posts.read)); 19 | router.get('/posts/slug/:slug', api.http(api.posts.read)); 20 | router.put('/posts/:id', api.http(api.posts.edit)); 21 | router.del('/posts/:id', api.http(api.posts.destroy)); 22 | 23 | // ## Settings 24 | router.get('/settings', api.http(api.settings.browse)); 25 | router.get('/settings/:key', api.http(api.settings.read)); 26 | router.put('/settings', api.http(api.settings.edit)); 27 | 28 | // ## Users 29 | router.get('/users', api.http(api.users.browse)); 30 | router.get('/users/:id', api.http(api.users.read)); 31 | router.get('/users/slug/:slug', api.http(api.users.read)); 32 | router.get('/users/email/:email', api.http(api.users.read)); 33 | router.put('/users/password', api.http(api.users.changePassword)); 34 | router.put('/users/owner', api.http(api.users.transferOwnership)); 35 | router.put('/users/:id', api.http(api.users.edit)); 36 | router.post('/users', api.http(api.users.add)); 37 | router.del('/users/:id', api.http(api.users.destroy)); 38 | 39 | // ## Tags 40 | router.get('/tags', api.http(api.tags.browse)); 41 | 42 | // ## Roles 43 | router.get('/roles/', api.http(api.roles.browse)); 44 | 45 | // ## Slugs 46 | router.get('/slugs/:type/:name', api.http(api.slugs.generate)); 47 | 48 | // ## Themes 49 | router.get('/themes', api.http(api.themes.browse)); 50 | router.put('/themes/:name', api.http(api.themes.edit)); 51 | 52 | // ## Notifications 53 | router.get('/notifications', api.http(api.notifications.browse)); 54 | router.post('/notifications', api.http(api.notifications.add)); 55 | router.del('/notifications/:id', api.http(api.notifications.destroy)); 56 | 57 | // ## DB 58 | router.get('/db', api.http(api.db.exportContent)); 59 | router.post('/db', middleware.busboy, api.http(api.db.importContent)); 60 | router.del('/db', api.http(api.db.deleteAllContent)); 61 | 62 | // ## Mail 63 | router.post('/mail', api.http(api.mail.send)); 64 | router.post('/mail/test', function (req, res) { 65 | api.http(api.mail.sendTest)(req, res); 66 | }); 67 | 68 | // ## Authentication 69 | router.post('/authentication/passwordreset', 70 | middleware.spamForgottenPrevention, 71 | api.http(api.authentication.generateResetToken) 72 | ); 73 | router.put('/authentication/passwordreset', api.http(api.authentication.resetPassword)); 74 | router.post('/authentication/invitation', api.http(api.authentication.acceptInvitation)); 75 | router.get('/authentication/invitation', api.http(api.authentication.isInvitation)); 76 | router.post('/authentication/setup', api.http(api.authentication.setup)); 77 | router.get('/authentication/setup', api.http(api.authentication.isSetup)); 78 | router.post('/authentication/token', 79 | middleware.spamSigninPrevention, 80 | middleware.addClientSecret, 81 | middleware.authenticateClient, 82 | middleware.generateAccessToken 83 | ); 84 | router.post('/authentication/revoke', api.http(api.authentication.revoke)); 85 | 86 | // ## Uploads 87 | router.post('/uploads', middleware.busboy, api.http(api.uploads.add)); 88 | 89 | return router; 90 | }; 91 | 92 | module.exports = apiRoutes; 93 | -------------------------------------------------------------------------------- /core/server/data/fixtures/permissions/index.js: -------------------------------------------------------------------------------- 1 | // # Permissions Fixtures 2 | // Sets up the permissions, and the default permissions_roles relationships 3 | var Promise = require('bluebird'), 4 | sequence = require('../../../utils/sequence'), 5 | _ = require('lodash'), 6 | errors = require('../../../errors'), 7 | models = require('../../../models'), 8 | fixtures = require('./permissions'), 9 | 10 | // private 11 | logInfo, 12 | addAllPermissions, 13 | addAllRolesPermissions, 14 | addRolesPermissionsForRole, 15 | 16 | // public 17 | populate, 18 | to003; 19 | 20 | logInfo = function logInfo(message) { 21 | errors.logInfo('Migrations', message); 22 | }; 23 | 24 | addRolesPermissionsForRole = function (roleName) { 25 | var fixturesForRole = fixtures.permissions_roles[roleName], 26 | permissionsToAdd; 27 | 28 | return models.Role.forge({name: roleName}).fetch({withRelated: ['permissions']}).then(function (role) { 29 | return models.Permissions.forge().fetch().then(function (permissions) { 30 | if (_.isObject(fixturesForRole)) { 31 | permissionsToAdd = _.map(permissions.toJSON(), function (permission) { 32 | var objectPermissions = fixturesForRole[permission.object_type]; 33 | if (objectPermissions === 'all') { 34 | return permission.id; 35 | } else if (_.isArray(objectPermissions) && _.contains(objectPermissions, permission.action_type)) { 36 | return permission.id; 37 | } 38 | return null; 39 | }); 40 | } 41 | 42 | return role.permissions().attach(_.compact(permissionsToAdd)); 43 | }); 44 | }); 45 | }; 46 | 47 | addAllRolesPermissions = function () { 48 | var roleNames = _.keys(fixtures.permissions_roles), 49 | ops = []; 50 | 51 | _.each(roleNames, function (roleName) { 52 | ops.push(addRolesPermissionsForRole(roleName)); 53 | }); 54 | 55 | return Promise.all(ops); 56 | }; 57 | 58 | addAllPermissions = function (options) { 59 | var ops = []; 60 | _.each(fixtures.permissions, function (permissions, objectType) { 61 | _.each(permissions, function (permission) { 62 | ops.push(function () { 63 | permission.object_type = objectType; 64 | return models.Permission.add(permission, options); 65 | }); 66 | }); 67 | }); 68 | 69 | return sequence(ops); 70 | }; 71 | 72 | // ## Populate 73 | populate = function (options) { 74 | logInfo('Populating permissions'); 75 | // ### Ensure all permissions are added 76 | return addAllPermissions(options).then(function () { 77 | // ### Ensure all roles_permissions are added 78 | return addAllRolesPermissions(); 79 | }); 80 | }; 81 | 82 | // ## Update 83 | // Update permissions to 003 84 | // Need to rename old permissions, and then add all of the missing ones 85 | to003 = function (options) { 86 | var ops = []; 87 | 88 | logInfo('Upgrading permissions'); 89 | 90 | // To safely upgrade, we need to clear up the existing permissions and permissions_roles before recreating the new 91 | // full set of permissions defined as of version 003 92 | models.Permissions.forge().fetch().then(function (permissions) { 93 | logInfo('Removing old permissions'); 94 | permissions.each(function (permission) { 95 | ops.push(permission.related('roles').detach().then(function () { 96 | return permission.destroy(); 97 | })); 98 | }); 99 | }); 100 | 101 | // Now we can perfom the normal populate 102 | return Promise.all(ops).then(function () { 103 | return populate(options); 104 | }); 105 | }; 106 | 107 | module.exports = { 108 | populate: populate, 109 | to003: to003 110 | }; 111 | -------------------------------------------------------------------------------- /core/server/apps/index.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('lodash'), 3 | Promise = require('bluebird'), 4 | errors = require('../errors'), 5 | api = require('../api'), 6 | loader = require('./loader'), 7 | // Holds the available apps 8 | availableApps = {}; 9 | 10 | function getInstalledApps() { 11 | return api.settings.read({context: {internal: true}, key: 'installedApps'}).then(function (response) { 12 | var installed = response.settings[0]; 13 | 14 | installed.value = installed.value || '[]'; 15 | 16 | try { 17 | installed = JSON.parse(installed.value); 18 | } catch (e) { 19 | return Promise.reject(e); 20 | } 21 | 22 | return installed; 23 | }); 24 | } 25 | 26 | function saveInstalledApps(installedApps) { 27 | return getInstalledApps().then(function (currentInstalledApps) { 28 | var updatedAppsInstalled = _.uniq(installedApps.concat(currentInstalledApps)); 29 | 30 | return api.settings.edit({settings: [{key: 'installedApps', value: updatedAppsInstalled}]}, {context: {internal: true}}); 31 | }); 32 | } 33 | 34 | module.exports = { 35 | init: function () { 36 | var appsToLoad; 37 | 38 | try { 39 | // We have to parse the value because it's a string 40 | api.settings.read({context: {internal: true}, key: 'activeApps'}).then(function (response) { 41 | var aApps = response.settings[0]; 42 | 43 | appsToLoad = JSON.parse(aApps.value) || []; 44 | }); 45 | } catch (e) { 46 | errors.logError( 47 | 'Failed to parse activeApps setting value: ' + e.message, 48 | 'Your apps will not be loaded.', 49 | 'Check your settings table for typos in the activeApps value. It should look like: ["app-1", "app2"] (double quotes required).' 50 | ); 51 | 52 | return Promise.resolve(); 53 | } 54 | 55 | // Grab all installed apps, install any not already installed that are in appsToLoad. 56 | return getInstalledApps().then(function (installedApps) { 57 | var loadedApps = {}, 58 | recordLoadedApp = function (name, loadedApp) { 59 | // After loading the app, add it to our hash of loaded apps 60 | loadedApps[name] = loadedApp; 61 | 62 | return Promise.resolve(loadedApp); 63 | }, 64 | loadPromises = _.map(appsToLoad, function (app) { 65 | // If already installed, just activate the app 66 | if (_.contains(installedApps, app)) { 67 | return loader.activateAppByName(app).then(function (loadedApp) { 68 | return recordLoadedApp(app, loadedApp); 69 | }); 70 | } 71 | 72 | // Install, then activate the app 73 | return loader.installAppByName(app).then(function () { 74 | return loader.activateAppByName(app); 75 | }).then(function (loadedApp) { 76 | return recordLoadedApp(app, loadedApp); 77 | }); 78 | }); 79 | 80 | return Promise.all(loadPromises).then(function () { 81 | // Save our installed apps to settings 82 | return saveInstalledApps(_.keys(loadedApps)); 83 | }).then(function () { 84 | // Extend the loadedApps onto the available apps 85 | _.extend(availableApps, loadedApps); 86 | }).catch(function (err) { 87 | errors.logError( 88 | err.message || err, 89 | 'The app will not be loaded', 90 | 'Check with the app creator, or read the app documentation for more details on app requirements' 91 | ); 92 | }); 93 | }); 94 | }, 95 | availableApps: availableApps 96 | }; 97 | -------------------------------------------------------------------------------- /core/server/api/themes.js: -------------------------------------------------------------------------------- 1 | // # Themes API 2 | // RESTful API for Themes 3 | var Promise = require('bluebird'), 4 | _ = require('lodash'), 5 | canThis = require('../permissions').canThis, 6 | config = require('../config'), 7 | errors = require('../errors'), 8 | settings = require('./settings'), 9 | themes; 10 | 11 | /** 12 | * ## Themes API Methods 13 | * 14 | * **See:** [API Methods](index.js.html#api%20methods) 15 | */ 16 | themes = { 17 | /** 18 | * ### Browse 19 | * Get a list of all the available themes 20 | * @param {{context}} options 21 | * @returns {Promise(Themes)} 22 | */ 23 | browse: function browse(options) { 24 | options = options || {}; 25 | 26 | return canThis(options.context).browse.theme().then(function () { 27 | return Promise.all([ 28 | settings.read({key: 'activeTheme', context: {internal: true}}), 29 | config.paths.availableThemes 30 | ]).then(function (result) { 31 | var activeTheme = result[0].settings[0].value, 32 | availableThemes = result[1], 33 | themes = [], 34 | themeKeys = Object.keys(availableThemes); 35 | 36 | _.each(themeKeys, function (key) { 37 | if (key.indexOf('.') !== 0 38 | && key !== '_messages' 39 | && key !== 'README.md' 40 | ) { 41 | var item = { 42 | uuid: key 43 | }; 44 | 45 | if (availableThemes[key].hasOwnProperty('package.json')) { 46 | item = _.merge(item, availableThemes[key]['package.json']); 47 | } 48 | 49 | item.active = item.uuid === activeTheme; 50 | 51 | themes.push(item); 52 | } 53 | }); 54 | 55 | return {themes: themes}; 56 | }); 57 | }, function () { 58 | return Promise.reject(new errors.NoPermissionError('You do not have permission to browse themes.')); 59 | }); 60 | }, 61 | 62 | /** 63 | * ### Edit 64 | * Change the active theme 65 | * @param {Theme} object 66 | * @param {{context}} options 67 | * @returns {Promise(Theme)} 68 | */ 69 | edit: function edit(object, options) { 70 | var themeName; 71 | 72 | // Check whether the request is properly formatted. 73 | if (!_.isArray(object.themes)) { 74 | return Promise.reject({type: 'BadRequest', message: 'Invalid request.'}); 75 | } 76 | 77 | themeName = object.themes[0].uuid; 78 | 79 | return canThis(options.context).edit.theme().then(function () { 80 | return themes.browse(options).then(function (availableThemes) { 81 | var theme; 82 | 83 | // Check if the theme exists 84 | theme = _.find(availableThemes.themes, function (currentTheme) { 85 | return currentTheme.uuid === themeName; 86 | }); 87 | 88 | if (!theme) { 89 | return Promise.reject(new errors.BadRequestError('Theme does not exist.')); 90 | } 91 | 92 | // Activate the theme 93 | return settings.edit( 94 | {settings: [{key: 'activeTheme', value: themeName}]}, {context: {internal: true}} 95 | ).then(function () { 96 | theme.active = true; 97 | return {themes: [theme]}; 98 | }); 99 | }); 100 | }, function () { 101 | return Promise.reject(new errors.NoPermissionError('You do not have permission to edit themes.')); 102 | }); 103 | } 104 | }; 105 | 106 | module.exports = themes; 107 | --------------------------------------------------------------------------------