/*
 * These are the global generic form styles used throughout the Ghost admin,
 * but mainly in the settings pages. Don't fuck with them.
 *
 * Table of Contents:
 *
 * General
 * Buttons
 * Split Buttons
 * iCheck jQuery Plugin Styles
 *
 */

/* =============================================================================
   General
   ============================================================================= */

form {

    fieldset {
        border: none;
        margin:0 0 3em 0;
        padding:0;
    }

    legend {
        display: block;
        width: 100%;
        margin: 2em 0;
        border-bottom:$lightbrown 1px solid;
        font-size: 1.2em;
        line-height: 2.0em;
        color: $brown;
    }

    label {
        display:block;
        margin:1.5em 0;
        padding-left:140px;
        position:relative;

        b {
            display:inline-block;
            position: absolute;
            top:0.5em;
            left:0;
            width:120px;
            font-weight:bold;
            color:$brown;
            text-align: right;
        }
    }

    p {
        color: darken($brown, 5%);
        font-size: 1em;
        margin:0;
    }

    input, textarea, select {
        width: 260px;
        padding: 5px 7px;
        margin: 0;
        outline: 0;
        font-size: 1.1em;
        line-height: 1.4em;
        background: #fff;
        border: darken($lightbrown, 5%) 1px solid;
        border-radius: $rounded;
        @include transition(all 0.15s ease-in-out);
    }

    textarea {
        width:100%;
        max-width:340px;
        min-width:250px;
        height:auto;
        min-height: 80px;
    }

    input, select, textarea {
        margin-bottom:5px;
    }

    input[type="text"]:focus,
    input[type="email"]:focus,
    input[type="search"]:focus,
    input[type="tel"]:focus,
    input[type="url"]:focus,
    input[type="password"]:focus,
    input[type="number"]:focus,
    input[type="date"]:focus,
    input[type="month"]:focus,
    input[type="week"]:focus,
    input[type="time"]:focus,
    input[type="datetime"]:focus,
    input[type="datetime-local"]:focus,
    textarea:focus {
        border:$grey 1px solid;
        background:#fff;
        outline:none;
        outline-width:0;
    }

    select {
        width:270px;
        height:30px;
        line-height:30px;
    }

    .radio input[type="radio"],
    .checkbox input[type="checkbox"] {
        float: left;
        width:auto;
        margin-right:6px;
        margin-top:4px;
        font-size:1em;
    }

    .radio, .checkbox {
        width:auto;
        margin: 5px 0;
        font-weight: normal;
        padding:0.55em 0;
        cursor: pointer;
    }

}//form

/* =============================================================================
   Buttons
   ============================================================================= */

/*
 * Buttons are used for primary calls to action on a page.
 *
 * Usage:
 * <button type="button" class="button">Default</button>
 */

// This base style is used on all buttons
%button {
    @include box-sizing(border-box);
    min-height: 35px;
    width:auto;
    display: inline-block;
    padding: 0.9em 1.37em;
    cursor: pointer;
    text-decoration: none;
    color: #fff;
    font-size: 11px; // Hack because Firefox sucks ass.
    line-height: 13px; // Hack because Firefox sucks ass.
    font-weight: 300;
    text-align: center;
    letter-spacing: 1px;
    text-transform: uppercase;
    text-shadow: none;
    border-radius: 0.2em;
    border: rgba(0,0,0,0.05) 0.1em solid;
    @include transition(background 0.3s ease, border-color 0.3s ease);

    &:hover {
        border-color: transparent;
        background: #f8f8f8;
        text-decoration: none;
    }
}

// This is the default button style
.button,
button,
input[type="button"] {
    @extend %button;
    color:#777;
    font-weight: normal;
    background: #eee;
    box-shadow: none;
    &:hover {
        border-color: rgba(0,0,0,0.1);
    }
}

// Button for save/next/continue/confirm actions
.button-save,
button[type="submit"],
input[type="submit"] {
    @extend %button;
    background: $blue;
    box-shadow: none;
    &:hover {background: darken($blue, 10%);}
}

// Button for actions which add stuff
.button-add {
    @extend %button;
    background: $green;
    &:hover {background: darken($green, 8%);}
}

// Button for deleting/removing stuff
.button-delete,
button[type="reset"],
input[type="reset"] {
    @extend %button;
    background: $red;
    box-shadow: none;
    &:hover {background: darken($red, 10%);}
}

// Alternative button with more visual attention, but no extra semantic meaning
.button-alt {
    @extend %button;
    background: lighten($darkgrey, 10%);
    &:hover {background: $darkgrey;}
}

// This applies normal link styles to de-emphasise a button
.button-link {
    @extend %button;
    color: $blue;
    background: transparent;
    border: none;
    &:hover {
        background: transparent;
        text-decoration: underline;
    }
}

/* =============================================================================
   Split Buttons
   ============================================================================= */

/*
 * The splitbutton adds addition values to a button, via a dropdown (or drop-up).
 *
 * Usage:
 *

 *     <button type="button" class="button">Split Up</button>
 *     Options
 *

 */

// These are the base styles applied to all splitbuttons
%splitbutton {
    display: inline-block;
    position: relative;
    font-size: 0; // hack to stop space after button
    white-space: nowrap;

    button {
        font-size: 11px; // hack to restore font size
        @include border-right-radius(0);
    }

    // This is the additional dropdown arrow, to the right of the button.
    .options {
        display: inline-block;
        position:relative;
        width: 35px;
        height: 35px;
        margin-left: -1px;
        vertical-align: top;
        text-align: center;
        color: #fff;
        background: #e5e5e5;
        border-radius: 0 2px 2px 0;
        box-shadow:
            rgba(0,0,0,0.02) 0 1px 0 inset,
            rgba(0,0,0,0.02) -1px 0 0 inset,
            rgba(0,0,0,0.02) 0 -1px 0 inset;
        @include icon($i-chevron-down, 9px) {
            position: absolute;
            top: 50%;
            right: 50%;
            margin-top: -3px;
            margin-right: -5px;
            @include transition(transform 0.3s ease, margin-top 0.3s ease);
        };

        // Spin the arrow on hover
        &:hover {
            box-shadow: none;
            background: #f8f8f8;
            @include icon($i-chevron-down) {
                @include transform(rotate(360deg));
            };
        }

        // If it has a class of "up" spin it an extra 180degress to point up
        &.up:hover {
            @include icon($i-chevron-down) {
                margin-top:-4px;
                @include transform(rotate(540deg));
                @include transition(transform 0.6s ease);
            };
        }
    }
}

// The default splitbutton
.splitbutton {
    @extend %splitbutton;
    .options {
        color:#777;
        &:hover {
            box-shadow:
                rgba(0,0,0,0.07) 0 1px 0 inset,
                rgba(0,0,0,0.07) -1px 0 0 inset,
                rgba(0,0,0,0.07) 0 -1px 0 inset;
        }
    }
}

// For save/next/continue/confirm actions
.splitbutton-save {
    @extend %splitbutton;
    .options {
        background: darken($blue, 5%);
        &:hover {background: darken($blue, 10%);}
    }
}

// For actions which add something
.splitbutton-add {
    @extend %splitbutton;
    .options {
        background: darken($green, 6%);
        &:hover {background: darken($green, 8%);}
    }
}

// For actions which delete something
.splitbutton-delete {
    @extend %splitbutton;
    .options {
        background: darken($red, 6%);
        &:hover {background: darken($red, 10%);}
    }
}

// Alternative style with more visual attention, but no extra semantic meaning
.splitbutton-alt {
    @extend %splitbutton;
    .options {
        background: lighten($darkgrey, 4%);
        &:hover {background: $darkgrey;}
    }
}

/* =============================================================================
   iCheck jQuery Plugin Styles
   ============================================================================= */
.icheckbox_ghost,
.iradio_ghost {
    @include box-sizing(border-box);
    display: block;
    width: 16px;
    height: 16px;
    float: left;
    margin: 2px 0 0 0;
    padding: 0;
    margin-right: 6px;
    background: none;
    border: none;
    box-shadow: none;
    border-radius: 2px;
    @include transition(
        border ease 0.25s,
        background ease 0.2s
    );
}

.icheckbox_ghost {
    border: 1px solid darken($lightbrown, 5%);
}
.icheckbox_ghost.hover {
    border: $brown 1px solid;
}
.icheckbox_ghost.checked {
    background: $blue;
    border: $blue 1px solid;
    padding:0;
    @include icon($i-check, 12px, #fff) {
        position: relative;
        top: -4px;
        left: -1px
    }
}
.icheckbox_ghost.disabled {
    border: 1px solid $lightbrown;
}
.icheckbox_ghost.checked.disabled {
    background: $lightbrown;
}

.iradio_ghost {
    border-radius: 11px;
    border: 1px solid darken($lightbrown, 5%);
}
.iradio_ghost.hover {
    border: $blue 2px solid;
}
.iradio_ghost.checked {
    background: $blue;
    border: none;
    @include icon($i-check, 1em, #fff);
}
.iradio_ghost.disabled {
    border: 1px solid $lightbrown;
    cursor: default;
}
.iradio_ghost.checked.disabled {
    background: $lightbrown;
}


/*global window, document, Ghost, $, _, Backbone */
(function () {
    "use strict";

    var Settings = {};

    // Base view
    // ----------
    Ghost.Views.Settings = Ghost.View.extend({
        initialize: function (options) {
            $(".settings-content").removeClass('active');
            this.addSubview(new Settings.Sidebar({
                el: '.settings-sidebar',
                pane: options.pane,
                model: this.model
            }));

            this.$('input').iCheck({
                checkboxClass: 'icheckbox_ghost'
            });
        }
    });

    // Sidebar (tabs)
    // ---------------
    Settings.Sidebar = Ghost.View.extend({
        initialize: function (options) {
            this.menu = this.$('.settings-menu');
            this.showContent(options.pane || 'general');
        },

        events: {
            'click .settings-menu li' : 'switchPane'
        },

        switchPane: function (e) {
            e.preventDefault();
            var item = $(e.currentTarget),
                id = item.find('a').attr('href').substring(1);
            this.showContent(id);
        },

        showContent: function (id) {
            Backbone.history.navigate('/settings/' + id);
            if (this.pane && '#' + id === this.pane.el) {
                return;
            }
            _.result(this.pane, 'destroy');
            this.setActive(id);
            this.pane = new Settings[id]({ model: this.model });
            this.pane.render();
        },

        setActive: function (id) {
            this.menu.find('li').removeClass('active');
            this.menu.find('a[href=#' + id + ']').parent().addClass('active');
        }
    });

    // Content panes
    // --------------
    Settings.Pane = Ghost.View.extend({
        destroy: function () {
            this.$el.removeClass('active');
            this.undelegateEvents();
        },

        render: function () {
            this.$el.addClass('active');
        }
    });

    // TODO: render templates on the client
    // TODO: use some kind of data-binding for forms

    // ### General settings
    Settings.general = Settings.Pane.extend({
        el: '#general',
        events: {
            'click .button-save': 'saveSettings'
        },

        saveSettings: function () {
            var self = this;
            this.model.save({
                title: this.$('#blog-title').val(),
                email: this.$('#email-address').val()
            }, {
                success: function () {
                    self.addSubview(new Ghost.Views.NotificationCollection({
                        model: [{
                            type: 'success',
                            message: 'Saved',
                            status: 'passive'
                        }]
                    }));
                },
                error: function () {
                    self.addSubview(new Ghost.Views.NotificationCollection({
                        model: [{
                            type: 'error',
                            message: 'Something went wrong, not saved :(',
                            status: 'passive'
                        }]
                    }));
                }
            });
        },

        render: function () {
            var settings = this.model.toJSON();
            this.$('#blog-title').val(settings.title);
            this.$('#email-address').val(settings.email);
            Settings.Pane.prototype.render.call(this);
        }
    });

    // ### Content settings
    Settings.content = Settings.Pane.extend({
        el: '#content',
        events: {
            'click .button-save': 'saveSettings'
        },
        saveSettings: function () {
            this.model.save({
                description: this.$('#blog-description').val()
            }, {
                success: function () {
                    this.addSubview(new Ghost.Views.NotificationCollection({
                        model: [{
                            type: 'success',
                            message: 'Saved',
                            status: 'passive'
                        }]
                    }));

                },
                error: function () {
                    this.addSubview(new Ghost.Views.NotificationCollection({
                        model: [{
                            type: 'error',
                            message: 'Something went wrong, not saved :(',
                            status: 'passive'
                        }]
                    }));
                }
            });
        },

        render: function () {
            var settings = this.model.toJSON();
            this.$('#blog-description').val(settings.description);
            Settings.Pane.prototype.render.call(this);
        }
    });

    // ### User settings
    Settings.users = Settings.Pane.extend({
        el: '#users',
        events: {
        }
    });

    // ### Appearance settings
    Settings.appearance = Settings.Pane.extend({
        el: '#appearance',
        events: {
        }
    });

    // ### Services settings
    Settings.services = Settings.Pane.extend({
        el: '#services',
        events: {
        }
    });

    // ### Plugins settings
    Settings.plugins = Settings.Pane.extend({
        el: '#plugins',
        events: {
        }
    });

}());


var User = require('./user').User,
    Permission = require('./permission').Permission,
    GhostBookshelf = require('./base'),
    Role,
    Roles;

Role = GhostBookshelf.Model.extend({
    tableName: 'roles',

    users: function () {
        return this.belongsToMany(User);
    },

    permissions: function () {
        return this.belongsToMany(Permission);
    }
});

Roles = GhostBookshelf.Collection.extend({
    model: Role
});

module.exports = {
    Role: Role,
    Roles: Roles
};


keep keep keep keep keep replace keep keep keep keep keep

ghost.app().configure("development", function () {

// # Ghost main app file

// Module dependencies.
var express = require('express'),
    when = require('when'),
    _ = require('underscore'),
    colors = require("colors"),
    semver = require("semver"),
    errors = require('./core/server/errorHandling'),
    admin = require('./core/server/controllers/admin'),
    frontend = require('./core/server/controllers/frontend'),
    api = require('./core/server/api'),
    flash = require('connect-flash'),
    Ghost = require('./core/ghost'),
    I18n = require('./core/shared/lang/i18n'),
    filters = require('./core/server/filters'),
    helpers = require('./core/server/helpers'),
    packageInfo = require('./package.json'),

// ## Custom Middleware
    auth,
    authAPI,
    isGhostAdmin,
    ghostLocals,
    disableCachedResult,

// ## Variables
    loading = when.defer(),

    /**
     * Create new Ghost object
     * @type {Ghost}
     */
    ghost = new Ghost();

/**
 * Authenticate a request by redirecting to login if not logged in
 * We strip /ghost/ out of the redirect parameter for neatness
 *
 * @type {*}
 */
auth = function (req, res, next) {
    if (!req.session.user) {
        var path = req.path.replace(/^\/ghost\/?/gi, ''),
            redirect = '';

        if (path !== '') {
            req.flash('warn', "Please login");
            redirect = '?r=' + encodeURIComponent(path);
        }

        return res.redirect('/ghost/login/' + redirect);
    }

    next();
};

/**
 * Authenticate a request by responding with a 401 and json error details
 *
 * @type {*}
 */
authAPI = function (req, res, next) {
    if (!req.session.user) {
        // TODO: standardize error format/codes/messages
        var err = { code: 42, message: 'Please login' };
        res.json(401, { error: err });
        return;
    }
    next();
};

// #### isGhostAdmin
// Middleware which uses the URL to detect whether this response should be an admin response
// This is used to ensure the right content is served, and is not for security purposes
isGhostAdmin = function (req, res, next) {
    res.isAdmin = /(^\/ghost$|^\/ghost\/)/.test(req.url);

    next();
};

// Expose the standard locals that every external page should have available,
// separating between the frontend / theme and the admin
ghostLocals = function (req, res, next) {
    // Make sure we have a locals value.
    res.locals = res.locals || {};
    res.locals.version = packageInfo.version;

    if (!res.isAdmin) {
        // filter the navigation items
        ghost.doFilter('ghostNavItems', {path: req.path, navItems: []}, function (navData) {
            // pass the theme navigation items and settings
            _.extend(res.locals, navData, {
                settings: ghost.settings()
            });

            next();
        });
    } else {
        _.extend(res.locals,  {
            // pass the admin flash messages, settings and paths
            messages: ghost.notifications,
            settings: ghost.settings(),
            availableThemes: ghost.paths().availableThemes,
            availablePlugins: ghost.paths().availablePlugins
        });

        next();
    }
};

// Disable any caching until it can be done properly
disableCachedResult = function (req, res, next) {
    res.set({
        "Cache-Control": "no-cache, must-revalidate",
        "Expires": "Sat, 26 Jul 1997 05:00:00 GMT"
    });

    next();
};

ghost.app().configure(function() {
    ghost.app().use(isGhostAdmin);
    ghost.app().use(express.favicon(__dirname + '/content/images/favicon.ico'));
    ghost.app().use(I18n.load(ghost));
    ghost.app().use(express.bodyParser({}));
    ghost.app().use(express.cookieParser('try-ghost'));
    ghost.app().use(express.cookieSession({ cookie: { maxAge: 60000000 }}));
    ghost.app().use(ghost.initTheme(ghost.app()));
    ghost.app().use(flash());

    if (process.env.NODE_ENV !== "development") {
        ghost.app().use(express.logger());
        ghost.app().use(express.errorHandler({ dumpExceptions: false, showStack: false }));
    }
});

ghost.app().configure("development", function() {
    ghost.app().use(express.errorHandler({ dumpExceptions: true, showStack: true }));
    ghost.app().use(express.logger('dev'));
});

// Expose the promise we will resolve after our pre-loading
ghost.loaded = loading.promise;

when.all([ghost.init(), filters.loadCoreFilters(ghost), helpers.loadCoreHelpers(ghost)]).then(function () {

    // post init config
    ghost.app().use(ghostLocals);

    /**
     * API routes..
     * @todo auth should be public auth not user auth
     */
    ghost.app().get('/api/v0.1/posts', authAPI, disableCachedResult, api.requestHandler(api.posts.browse));
    ghost.app().post('/api/v0.1/posts', authAPI, disableCachedResult, api.requestHandler(api.posts.add));
    ghost.app().get('/api/v0.1/posts/:id', authAPI, disableCachedResult, api.requestHandler(api.posts.read));
    ghost.app().put('/api/v0.1/posts/:id', authAPI, disableCachedResult, api.requestHandler(api.posts.edit));
    ghost.app().del('/api/v0.1/posts/:id', authAPI, disableCachedResult, api.requestHandler(api.posts.destroy));
    ghost.app().get('/api/v0.1/settings', authAPI, disableCachedResult, api.cachedSettingsRequestHandler(api.settings.browse));
    ghost.app().get('/api/v0.1/settings/:key', authAPI, disableCachedResult, api.cachedSettingsRequestHandler(api.settings.read));
    ghost.app().put('/api/v0.1/settings', authAPI, disableCachedResult, api.cachedSettingsRequestHandler(api.settings.edit));

    /**
     * Admin routes..
     * @todo put these somewhere in admin
     */
    ghost.app().get(/^\/logout\/?$/, admin.logout);
    ghost.app().get('/ghost/login/', admin.login);
    ghost.app().get('/ghost/signup/', admin.signup);
    ghost.app().post('/ghost/login/', admin.auth);
    ghost.app().post('/ghost/signup/', admin.doRegister);
    ghost.app().get('/ghost/editor/:id', auth, admin.editor);
    ghost.app().get('/ghost/editor', auth, admin.editor);
    ghost.app().get('/ghost/content', auth, admin.content);
    ghost.app().get('/ghost/settings*', auth, admin.settings);
    ghost.app().get('/ghost/debug', auth, admin.debug.index);
    ghost.app().get('/ghost/debug/db/export/', auth, admin.debug['export']);
    ghost.app().post('/ghost/debug/db/import/', auth, admin.debug.import);
    ghost.app().get('/ghost/debug/db/reset/', auth, admin.debug.reset);
    ghost.app().get(/^\/(ghost$|(ghost-admin|admin|wp-admin|dashboard|login)\/?)/, auth, function (req, res) {
        res.redirect('/ghost/');
    });
    ghost.app().get('/ghost/', auth, admin.index);

    // Notifications routes
    ghost.app().del('/api/v0.1/notifications/:id', authAPI, disableCachedResult, api.requestHandler(api.notifications.destroy));
    ghost.app().post('/api/v0.1/notifications/', authAPI, disableCachedResult, api.requestHandler(api.notifications.add));

    /**
     * Frontend routes..
     * @todo dynamic routing, homepage generator, filters ETC ETC
     */
    ghost.app().get('/:slug', frontend.single);
    ghost.app().get('/', frontend.homepage);
    ghost.app().get('/page/:page/', frontend.homepage);

    ghost.app().listen(
        ghost.config().env[process.env.NODE_ENV || 'development'].url.port,
        ghost.config().env[process.env.NODE_ENV || 'development'].url.host,
        function () {

            // Tell users if their node version is not supported, and exit
            if (!semver.satisfies(process.versions.node, packageInfo.engines.node)) {
                console.log(
                    "\n !!! INVALID NODE VERSION !!!\n".red,
                    "Ghost requires node version".red,
                    packageInfo.engines.node.yellow,
                    "as defined in package.json\n".red
                );

                process.exit(-1);
            }

            // Remove once software becomes suitably 'ready'
            console.log(
                "\n !!! ALPHA SOFTWARE WARNING !!!\n".red,
                "Ghost is in the early stages of development.\n".red,
                "Expect to see bugs and other issues (but please report them.)\n".red
            );

            console.log("Express server listening on address:",
                ghost.config().env[process.env.NODE_ENV || 'development'].url.host + ':'
                    + ghost.config().env[process.env.NODE_ENV || 'development'].url.port);

            // Let everyone know we have finished loading
            loading.resolve();
        }
    );
}, errors.logAndThrowError);


[submodule "content/themes/casper"]
	path = content/themes/casper
	url = git@github.com:TryGhost/Casper.git


    {{content.data.number.count}}
    {{content.data.number.sub.value}} {{content.data.number.sub.item}} {{content.data.number.sub.period}}


ghost.app().get(/^\/(ghost$|(ghost-admin|admin|wp-admin|dashboard|login)\/?)/, auth, function (req, res) {

keep keep keep keep keep replace keep keep keep keep


var _ = require('underscore'),
    errors;

/**
 * Basic error handling helpers
 */
errors = {
    throwError: function (err) {
        if (!err) {
            err = new Error("An error occurred");
        }

        if (_.isString(err)) {
            throw new Error(err);
        }

        throw err;
    },

    logError: function (err) {
        err = err || "Unknown";
        // TODO: Logging framework hookup
//        Eventually we'll have better logging which will know about envs
        if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'staging'
                || process.env.NODE_ENV === 'production') {
            console.log("Error occurred: ", err.message || err, err.stack || "");
        }
    },

    logAndThrowError: function (err) {
        this.logError(err);

        this.throwError(err);
    },

    logErrorWithMessage: function (msg) {
        var self = this;

        return function () {
            self.logError(msg);
        };
    },

    logErrorWithRedirect: function (msg, redirectTo, req, res) {
        var self = this;

        return function () {
            self.logError(msg);

            if (_.isFunction(res.redirect)) {
                res.redirect(redirectTo);
            }
        };
    }
};

// Ensure our 'this' context in the functions
_.bindAll(errors, "throwError", "logError", "logAndThrowError", "logErrorWithMessage", "logErrorWithRedirect");

module.exports = errors;


var when = require("when"),
    _ = require("underscore"),
    knex = require('../../models/base').Knex,
    Importer001;

Importer001 = function () {
    _.bindAll(this, "importFrom001");

    this.version = "001";

    this.importFrom = {
        "001": this.importFrom001
    };
};

Importer001.prototype.importData = function (data) {
    return this.canImport(data)
        .then(function (importerFunc) {
            return importerFunc(data);
        }, function (reason) {
            return when.reject(reason);
        });
};

Importer001.prototype.canImport = function (data) {
    if (data.meta && data.meta.version && this.importFrom[data.meta.version]) {
        return when.resolve(this.importFrom[data.meta.version]);
    }

    return when.reject("Unsupported version of data");
};

Importer001.prototype.importFrom001 = function (data) {
    var insertOps = [];

    _.each(data.data, function (tableData, name) {
        if (tableData && tableData.length) {
            insertOps.push(knex(name).insert(tableData));
        }
    });

    return when.all(insertOps).then(function (results) {
        return when.resolve(results);
    }, function (err) {
        console.log("Error inserting imported data: ", err.message || err, err.stack);
    });
};

module.exports = {
    Importer001: Importer001,
    importData: function (data) {
        new Importer001().importData(data);
    }
};


{{content.title}}
{{#if options.close}}Close{{/if}}

    {{#if options.confirm}}

        <button class="button-add js-button-accept">{{options.confirm.accept.text}}</button>
        <button class="button-delete js-button-reject">{{options.confirm.reject.text}}</button>

    {{/if}}


return "{gfm-js-extract-pre-" + hash + "}";

//prevent foo_bar and foo_bar_baz from ending up with an italic word in the middle

// Replace showdown's implementation of bold

/**
 * Client showdown integration tests
 *
 * Ensures that the final output from showdown + client extensions is as expected
 */

/*globals describe, it */
var testUtils = require('./testUtils'),
    should = require('should'),

    // Stuff we are testing
    Showdown = require('showdown'),
    github = require('../../shared/vendor/showdown/extensions/github'),
    ghostdown = require('../../client/assets/vendor/showdown/extensions/ghostdown'),
    converter = new Showdown.converter({extensions: [ghostdown, github]});

describe("Showdown client side converter", function () {

    it("should replace showdown strike through with html", function () {
        var testPhrase = {input: "~~foo_bar~~", output: /^
foo_bar<\/del><\/p>$/},
            processedMarkup = converter.makeHtml(testPhrase.input);

        // The image is the entire markup, so the image box should be too
        processedMarkup.should.match(testPhrase.output);
    });

    it("should not touch single underscores inside words", function () {
        var testPhrase = {input: "foo_bar", output: /^
foo_bar<\/p>$/},
            processedMarkup = converter.makeHtml(testPhrase.input);

        processedMarkup.should.match(testPhrase.output);
    });

    it("should not create italic words between lines", function () {
        var testPhrase = {input: "foo_bar\nbar_foo", output: /^
foo_bar
\nbar_foo<\/p>$/},
            processedMarkup = converter.makeHtml(testPhrase.input);

        processedMarkup.should.match(testPhrase.output);
    });

    it("should not touch underscores in code blocks", function () {
        var testPhrase = {input: "    foo_bar_baz", output: /^
foo_bar_baz\n<\/code><\/pre>$/},
            processedMarkup = converter.makeHtml(testPhrase.input);

        processedMarkup.should.match(testPhrase.output);
    });

    it("should not touch underscores in pre blocks", function () {
        var testPhrases = [
                {input: "
\nfoo_bar_baz\n
", output: /^
\nfoo_bar_baz\n<\/pre>$/},
                {input: "
foo_bar_baz
", output: /^
foo_bar_baz<\/pre>$/}
            ],
            processedMarkup;

        testPhrases.forEach(function (testPhrase) {
            processedMarkup = converter.makeHtml(testPhrase.input);
            processedMarkup.should.match(testPhrase.output);
        });
    });

    it("should not treat pre blocks with pre-text differently", function () {
        var testPhrases = [
                {input: "
\nthis is `a\\_test` and this\\_too and finally_this_is\n
", output: /^
\nthis is `a\\_test` and this\\_too and finally_this_is\n<\/pre>$/},
                {input: "hmm
\nthis is `a\\_test` and this\\_too and finally_this_is\n
", output: /^
hmm<\/p>\n\n
\nthis is `a\\_test` and this\\_too and finally_this_is\n<\/pre>$/}
            ],
            processedMarkup;

        testPhrases.forEach(function (testPhrase) {
            processedMarkup = converter.makeHtml(testPhrase.input);
            processedMarkup.should.match(testPhrase.output);
        });
    });

    it("should escape two or more underscores inside words", function () {
        var testPhrases = [
                {input: "foo_bar_baz", output: /^
foo_bar_baz<\/p>$/},
                {input: "foo_bar_baz_bat", output: /^
foo_bar_baz_bat<\/p>$/},
                {input: "foo_bar_baz_bat_boo", output: /^
foo_bar_baz_bat_boo<\/p>$/},
                {input: "FOO_BAR", output: /^
FOO_BAR<\/p>$/},
                {input: "FOO_BAR_BAZ", output: /^
FOO_BAR_BAZ<\/p>$/},
                {input: "FOO_bar_BAZ_bat", output: /^
FOO_bar_BAZ_bat<\/p>$/},
                {input: "FOO_bar_BAZ_bat_BOO", output: /^
FOO_bar_BAZ_bat_BOO<\/p>$/},
                {input: "foo_BAR_baz_BAT_boo", output: /^
foo_BAR_baz_BAT_boo<\/p>$/}
            ],
            processedMarkup;

        testPhrases.forEach(function (testPhrase) {
            processedMarkup = converter.makeHtml(testPhrase.input);
            processedMarkup.should.match(testPhrase.output);
        });
    });

    it("should turn newlines into br tags in simple cases", function () {
        var testPhrases = [
                {input: "fizz\nbuzz", output: /^
fizz
\nbuzz<\/p>$/},
                {input: "Hello world\nIt's a fine day", output: /^
Hello world
\nIt\'s a fine day<\/p>$/}
            ],
            processedMarkup;

        testPhrases.forEach(function (testPhrase) {
            processedMarkup = converter.makeHtml(testPhrase.input);
            processedMarkup.should.match(testPhrase.output);
        });
    });

    it("should convert newlines in all groups", function () {
        var testPhrases = [
                {input: "ruby\npython\nerlang", output: /^
ruby
\npython
\nerlang<\/p>$/},
                {input: "Hello world\nIt's a fine day\nout", output: /^
Hello world
\nIt\'s a fine day
\nout<\/p>$/}
            ],
            processedMarkup;

        testPhrases.forEach(function (testPhrase) {
            processedMarkup = converter.makeHtml(testPhrase.input);
            processedMarkup.should.match(testPhrase.output);
        });
    });

    it("should convert newlines in even long groups", function () {
        var testPhrases = [
                {input: "ruby\npython\nerlang\ngo", output: /^
ruby
\npython
\nerlang
\ngo<\/p>$/},
                {
                    input: "Hello world\nIt's a fine day\noutside\nthe window",
                    output: /^
Hello world
\nIt\'s a fine day
\noutside
\nthe window<\/p>$/
                }
            ],
            processedMarkup;

        testPhrases.forEach(function (testPhrase) {
            processedMarkup = converter.makeHtml(testPhrase.input);
            processedMarkup.should.match(testPhrase.output);
        });
    });

    it("should not convert newlines in lists", function () {
        var testPhrases = [
                {input: "#fizz\n# buzz\n### baz", output: /^
fizz<\/h1>\n\n
buzz<\/h1>\n\n
baz<\/h3>$/},
                {input: "* foo\n* bar", output: /^
\n
foo<\/li>\n
bar<\/li>\n<\/ul>$/}
            ],
            processedMarkup;

        testPhrases.forEach(function (testPhrase) {
            processedMarkup = converter.makeHtml(testPhrase.input);
            processedMarkup.should.match(testPhrase.output);
        });
    });

    it("should auto-link URL in text with markdown syntax", function () {
        var testPhrases = [
                {
                    input: "http://google.co.uk",
                    output: /^
http:\/\/google.co.uk<\/a><\/p>$/
                },
                {
                    input: "https://atest.com/fizz/buzz?baz=fizzbuzz",
                    output: /^
https:\/\/atest.com\/fizz\/buzz\?baz=fizzbuzz<\/a><\/p>$/
                },
                {
                    input: "Some [ text (http://www.google.co.uk) some other text",
                    output: /^
Some \[ text \(http:\/\/www.google.co.uk<\/a>\) some other text<\/p>$/
                },
                {
                    input: ">http://google.co.uk",
                    output: /^
\n
http:\/\/google.co.uk<\/a><\/p>\n<\/blockquote>$/
                },
                {
                    input: "> http://google.co.uk",
                    output: /^
\n
http:\/\/google.co.uk<\/a><\/p>\n<\/blockquote>$/
                },
                {
                    input: "<>>> http://google.co.uk",
                    output: /^
<>>> http:\/\/google.co.uk<\/a><\/p>$/
                },
                {
                    input: "http://google.co.uk",
                    output: /^
http:\/\/google.co.uk<\/a><\/p>$/
                },
                {
                    input: "# http://google.co.uk",
                    output: /^
http:\/\/google.co.uk<\/a><\/h1>$/
                },
                {
                    input: "* http://google.co.uk",
                    output: /^
\n
http:\/\/google.co.uk<\/a><\/li>\n<\/ul>$/
                }
            ],
            processedMarkup;

        testPhrases.forEach(function (testPhrase) {
            processedMarkup = converter.makeHtml(testPhrase.input);
            processedMarkup.should.match(testPhrase.output);
        });
    });

    it("should auto-link Email", function () {
        var testPhrase = {input: "info@tryghost.org", output: /^
info@tryghost.org<\/a><\/p>$/},
            processedMarkup = converter.makeHtml(testPhrase.input);

        processedMarkup.should.match(testPhrase.output);
    });

    it("should convert reference format URL", function () {
        var testPhrases = [
                {
                    input: "[Google][1]\n\n[1]: http://google.co.uk",
                    output: /^
Google<\/a><\/p>$/,
                },
                {
                    input: "[Google][1]\n\n[1]: http://google.co.uk \"some text\"",
                    output: /^
Google<\/a><\/p>$/
                },
                {
                    input: "[http://google.co.uk]: http://google.co.uk\n\n[Hello][http://google.co.uk]",
                    output: /^
Hello<\/a><\/p>$/
                }
            ],
            processedMarkup;

        testPhrases.forEach(function (testPhrase) {
            processedMarkup = converter.makeHtml(testPhrase.input);
            processedMarkup.should.match(testPhrase.output);
        });
    });

    it("should convert reference format image", function () {
        var testPhrases = [
                {
                    input: "![Google][1]\n\n[1]: http://dsurl.stuff/something.jpg",
                    output: /^<section.*?.*?<\/section>$/,
                },
                {
                    input: "![Google][1]\n\n[1]: http://dsurl.stuff/something.jpg \"some text\"",
                    output: /^<section.*?.*?<\/section>$/
                },
                {
                    input: "[http://www.google.co.uk]: http://www.google.co.uk\n\n![Hello][http://www.google.co.uk]",
                    output: /^<section.*?.*?<\/section>$/
                }
            ],
            processedMarkup;

        testPhrases.forEach(function (testPhrase) {
            processedMarkup = converter.makeHtml(testPhrase.input);
            processedMarkup.should.match(testPhrase.output);
        });
    });

    it("should NOT auto-link URL in HTML", function () {
        var testPhrases = [
                {
                    input: '',
                    output: /^
<\/p>$/
                },
                {
                    input: '',
                    output: /^
<\/p>$/
                },
                {
                    input: '',
                    output: /^


Copyright (c) 2013 Ghost Foundation - Released under The MIT License.

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:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

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
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.


    {{content.data.name}}
    {{contnet.data.handle}}

{{{content.data.tweet}}}

    <time class="tweet-time">{{content.data.time}}</time>


/*globals describe, beforeEach, afterEach, it*/
var testUtils = require('./testUtils'),
    should = require('should'),
    sinon = require('sinon'),
    when = require('when'),

    _ = require("underscore"),
    cp = require('child_process'),

    // Stuff we are testing
    Ghost = require('../../ghost'),
    defaultConfig = require('../../../config'),
    SMTP,
    SENDMAIL,
    fakeConfig,
    fakeSettings,
    fakeSendmail,
    sandbox = sinon.sandbox.create(),
    ghost;

// Mock SMTP config
SMTP = {
    transport: 'SMTP',
    options: {
        service: 'Gmail',
        auth: {
            user: 'nil',
            pass: '123'
        }
    }
};

// Mock Sendmail config
SENDMAIL = {
    transport: 'sendmail',
    options: {
        path: '/nowhere/sendmail'
    }
};

describe("Mail", function () {

    beforeEach(function () {
        // Mock config and settings
        fakeConfig = _.extend({}, defaultConfig);
        fakeSettings = {
            url: 'http://test.tryghost.org',
            email: 'ghost-test@localhost'
        };
        fakeSendmail = '/fake/bin/sendmail';

        ghost = new Ghost();

        sandbox.stub(ghost, "config", function () {
            return fakeConfig;
        });

        sandbox.stub(ghost, "settings", function () {
            return fakeSettings;
        });

        sandbox.stub(ghost.mail, "isWindows", function () {
            return false;
        });

        sandbox.stub(ghost.mail, "detectSendmail", function () {
            return when.resolve(fakeSendmail);
        });
    });

    afterEach(function () {
        sandbox.restore();
    });

    it('should attach mail provider to ghost instance', function () {
        should.exist(ghost.mail);
        ghost.mail.should.have.property('init');
        ghost.mail.should.have.property('transport');
        ghost.mail.should.have.property('send').and.be.a('function');
    });

    it('should setup SMTP transport on initialization', function (done) {
        fakeConfig.env[process.env.NODE_ENV].mail = SMTP;
        ghost.mail.init(ghost).then(function(){
            ghost.mail.should.have.property('transport');
            ghost.mail.transport.transportType.should.eql('SMTP');
            ghost.mail.transport.sendMail.should.be.a('function');
            done();
        }).then(null, done);
    });

    it('should setup sendmail transport on initialization', function (done) {
        fakeConfig.env[process.env.NODE_ENV].mail = SENDMAIL;
        ghost.mail.init(ghost).then(function(){
            ghost.mail.should.have.property('transport');
            ghost.mail.transport.transportType.should.eql('SENDMAIL');
            ghost.mail.transport.sendMail.should.be.a('function');
            done();
        }).then(null, done);
    });

    it('should fallback to sendmail if no config set', function (done) {
        fakeConfig.env[process.env.NODE_ENV].mail = null;
        ghost.mail.init(ghost).then(function(){
            ghost.mail.should.have.property('transport');
            ghost.mail.transport.transportType.should.eql('SENDMAIL');
            ghost.mail.transport.options.path.should.eql(fakeSendmail);
            done();
        }).then(null, done);
    });

    it('should fallback to sendmail if config is empty', function (done) {
        fakeConfig.env[process.env.NODE_ENV].mail = {};
        ghost.mail.init(ghost).then(function(){
            ghost.mail.should.have.property('transport');
            ghost.mail.transport.transportType.should.eql('SENDMAIL');
            ghost.mail.transport.options.path.should.eql(fakeSendmail);
            done();
        }).then(null, done);
    });

    it('should disable transport if config is empty & sendmail not found', function (done) {
        fakeConfig.env[process.env.NODE_ENV].mail = {};
        ghost.mail.detectSendmail.restore();
        sandbox.stub(ghost.mail, "detectSendmail", when.reject);
        ghost.mail.init(ghost).then(function(){
            should.not.exist(ghost.mail.transport);
            done();
        }).then(null, done);
    });

    it('should disable transport if config is empty & platform is win32', function (done) {
        fakeConfig.env[process.env.NODE_ENV].mail = {};
        ghost.mail.detectSendmail.restore();
        ghost.mail.isWindows.restore();
        sandbox.stub(ghost.mail, 'isWindows', function(){ return true });
        ghost.mail.init(ghost).then(function(){
            should.not.exist(ghost.mail.transport);
            done();
        }).then(null, done);
    });

    it('should fail to send messages when no transport is set', function (done) {
        ghost.mail.detectSendmail.restore();
        sandbox.stub(ghost.mail, "detectSendmail", when.reject);
        ghost.mail.init(ghost).then(function(){
            ghost.mail.send().then(function(){
                should.fail();
                done();
            }, function (err) {
                err.should.be.an.instanceOf(Error);
                done();
            });
        });
    });

    it('should fail to send messages when given insufficient data', function (done) {
        when.settle([
            ghost.mail.send(),
            ghost.mail.send({}),
            ghost.mail.send({ subject: '123' }),
            ghost.mail.send({ subject: '', html: '123' })
        ]).then(function (descriptors) {
            descriptors.forEach(function (d) {
                d.state.should.equal('rejected');
                d.reason.should.be.an.instanceOf(Error);
            });
            done();
        });
    });

});


//
//  Github Extension (WIP)
//  ~~strike-through~~   ->  strike-through
//

(function () {
    var github = function (converter) {
        return [
            {
                // strike-through
                // NOTE: showdown already replaced "~" with "~T", so we need to adjust accordingly.
                type    : 'lang',
                regex   : '(~T){2}([^~]+)(~T){2}',
                replace : function (match, prefix, content, suffix) {
                    return '' + content + '';
                }
            },
            {
                // GFM newline and underscore modifications
                type    : 'lang',
                filter  : function (text) {
                    var extractions = {},
                        hashID = 0;

                    function hashId() {
                        return hashID++;
                    }

                    // Extract pre blocks
                    text = text.replace(/
[\s\S]*?<\/pre>/gim, function (x) {
                        var hash = hashId();
                        extractions[hash] = x;
                        return "{gfm-js-extract-pre-" + hash + "}";
                    }, 'm');

                    //prevent foo_bar and foo_bar_baz from ending up with an italic word in the middle
                    text = text.replace(/(^(?! {4}|\t)\w+_\w[\w_]*)/gm, function (x) {
                        return x.replace(/_/gm, '\\_');
                    });

                    // in very clear cases, let newlines become
 tags
                    text = text.replace(/^[\w\<][^\n]*\n+/gm, function (x) {
                        return x.match(/\n{2}/) ? x : x.trim() + "  \n";
                    });

                    text = text.replace(/\{gfm-js-extract-pre-([0-9]+)\}/gm, function (x, y) {
                        return "\n\n" + extractions[y];
                    });

                    return text;
                }
            },
            {
                // Auto-link URLs and emails
                type    : 'lang',
                filter  : function (text) {
                    var extractions = {},
                        hashID = 0;

                    function hashId() {
                        return hashID++;
                    }

                    // filter out def urls
                    // from Marked https://github.com/chjj/marked/blob/master/lib/marked.js#L24
                    text = text.replace(/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/gmi,
                        function (x) {
                            var hash = hashId();
                            extractions[hash] = x;
                            return "{gfm-js-extract-ref-url-" + hash + "}";
                        });

                    // match a URL
                    // adapted from https://gist.github.com/jorilallo/1283095#L158
                    // and http://blog.stevenlevithan.com/archives/mimic-lookbehind-javascript
                    text = text.replace(/(\]\(|\]|\[|]*?\>)?https?\:\/\/[^"\s\<\>]*[^.,;'">\:\s\<\>\)\]\!]/gmi,
                        function (wholeMatch, lookBehind, matchIndex) {
                            // Check we are not inside an HTML tag
                            var left = text.slice(0, matchIndex), right = text.slice(matchIndex);
                            if ((left.match(/<[^>]+$/) && right.match(/^[^>]*>/)) || lookBehind) {
                                return wholeMatch;
                            }
                            // If we have a matching lookBehind, this is a failure, else wrap the match in  tag
                            return lookBehind ? wholeMatch : "" + wholeMatch + "";
                        });

                    // match emil
                    text = text.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/gmi, function (wholeMatch) {
                        return "" + wholeMatch + "";
                    });

                    text = text.replace(/\{gfm-js-extract-ref-url-([0-9]+)\}/gi, function (x, y) {
                        return "\n\n" + extractions[y];
                    });

                    return text;
                }
            }
        ];
    };

    // Client-side export
    if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) { window.Showdown.extensions.github = github; }
    // Server-side export
    if (typeof module !== 'undefined') module.exports = github;
}());


var when = require('when'),
    migration = require('../migration');

module.exports = function (version) {
    var exporter;

    if (version > migration.currentVersion) {
        return when.reject("Your data version is ahead of the current Ghost version. Please upgrade in order to export.");
    }

    try {
        exporter = require("./" + version);
    } catch (ignore) {
        // Zero effs given
    }

    if (!exporter) {
        return when.reject("No exporter found for data version " + version);
    }

    return exporter.exportData();
};


var _ = require("underscore"),
    when = require('when'),
    ghostApi,
    loader = require("./loader"),
    GhostPlugin = require("./GhostPlugin");

function getInstalledPlugins() {
    if (!ghostApi) {
        ghostApi = require('../api');
    }

    return ghostApi.settings.read("installedPlugins").then(function (installed) {
        installed.value = installed.value || "[]";

        try {
            installed = JSON.parse(installed.value);
        } catch (e) {
            return when.reject(e);
        }

        return installed;
    });
}

function saveInstalledPlugins(installedPlugins) {
    return getInstalledPlugins().then(function (currentInstalledPlugins) {
        var updatedPluginsInstalled = _.uniq(installedPlugins.concat(currentInstalledPlugins));

        return ghostApi.settings.edit("installedPlugins", updatedPluginsInstalled);
    });
}

module.exports = {
    GhostPlugin: GhostPlugin,

    init: function (ghost, pluginsToLoad) {
        // Grab all installed plugins, install any not already installed that are in pluginsToLoad.
        return getInstalledPlugins().then(function (installedPlugins) {
            var loadedPlugins = {},
                recordLoadedPlugin = function (name, loadedPlugin) {
                    // After loading the plugin, add it to our hash of loaded plugins
                    loadedPlugins[name] = loadedPlugin;

                    return when.resolve(loadedPlugin);
                },
                loadPromises = _.map(pluginsToLoad, function (plugin) {
                    // If already installed, just activate the plugin
                    if (_.contains(installedPlugins, plugin)) {
                        return loader.activatePluginByName(plugin, ghost).then(function (loadedPlugin) {
                            return recordLoadedPlugin(plugin, loadedPlugin);
                        });
                    }

                    // Install, then activate the plugin
                    return loader.installPluginByName(plugin, ghost).then(function () {
                        return loader.activatePluginByName(plugin, ghost);
                    }).then(function (loadedPlugin) {
                        return recordLoadedPlugin(plugin, loadedPlugin);
                    });
                });

            return when.all(loadPromises).then(function () {
                // Save our installed plugins to settings
                return saveInstalledPlugins(_.keys(loadedPlugins));
            }).then(function () {
                // Return the hash of all loaded plugins
                return when.resolve(loadedPlugins);
            });
        });
    }
};


/**
 * Tests logging out and attempting to sign up
 */

/*globals casper, __utils__, url, testPost, falseUser, email */
casper.test.begin('Redirects page 1 request', 1, function suite(test) {
    test.filename = '01_route_test_redirects_page_1.png';

    casper.start(url + 'page/1/', function then(response) {
        test.assertEqual(casper.getCurrentUrl().indexOf('page/'), -1, 'Should be redirected to "/".');
    }).viewport(1280, 1024);

    casper.run(function () {
        test.done();
    });
});


// in very clear cases, let newlines become <br /> tags

text = text.replace(/^[\w\<][^\n]*\n+/gm, function (x) {

return x.match(/\n{2}/) ? x : x.trim() + "  \n";

// Replace showdown's implementation of emphasis

// requires a negative lookbehind for \n before the final * to prevent breaking lists

return escaped || newline ? match : m1 ? "" + m1 + "" : "" + m2 + "";


/*global window, document, Ghost, $, _, Backbone */
(function () {
    "use strict";

    Ghost.Models.Post = Backbone.Model.extend({

        defaults: {
            status: 'draft'
        },

        blacklist: ['published', 'draft'],

        parse: function (resp) {
            if (resp.status) {
                resp.published = !!(resp.status === "published");
                resp.draft = !!(resp.status === "draft");
            }
            if (resp.tags) {
                // TODO: parse tags into it's own collection on the model (this.tags)
                return resp;
            }
            return resp;
        },

        validate: function (attrs) {
            if (_.isEmpty(attrs.title)) {
                return 'You must specify a title for the post.';
            }
        },

        addTag: function (tagToAdd) {
            var tags = this.get('tags') || [];
            tags.push(tagToAdd);
            this.set('tags', tags);
        },

        removeTag: function (tagToRemove) {
            var tags = this.get('tags') || [];
            tags = _.reject(tags, function (tag) {
                return tag.id === tagToRemove.id || tag.name === tagToRemove.name;
            });
            this.set('tags', tags);
        }
    });

    Ghost.Collections.Posts = Backbone.Collection.extend({
        url: Ghost.settings.apiRoot + '/posts',
        model: Ghost.Models.Post,
        parse: function (resp) {
            if (_.isArray(resp.posts)) {
                this.limit = resp.limit;
                this.currentPage = resp.page;
                this.totalPages = resp.pages;
                this.totalPosts = resp.total;
                this.nextPage = resp.next;
                this.prevPage = resp.prev;
                return resp.posts;
            }
            return resp;
        }
    });

}());


/*global window, document, Ghost, $, _, Backbone, JST */
(function () {
    "use strict";

    var ContentList,
        ContentItem,
        PreviewContainer;

    // Base view
    // ----------
    Ghost.Views.Blog = Ghost.View.extend({
        initialize: function (options) {
            this.addSubview(new PreviewContainer({ el: '.js-content-preview', collection: this.collection })).render();
            this.addSubview(new ContentList({ el: '.js-content-list', collection: this.collection })).render();
        }
    });

    // Content list (sidebar)
    // -----------------------
    ContentList = Ghost.View.extend({

        events: {
            'click .content-list-content'    : 'scrollHandler'
        },

        initialize: function (options) {
            this.$('.content-list-content').scrollClass({target: '.content-list', offset: 10});
            this.listenTo(this.collection, 'remove', this.showNext);
        },

        showNext: function () {
            var id = this.collection.at(0).id;
            if (id) {
                Backbone.trigger('blog:activeItem', id);
            }
        },

        render: function () {
            this.collection.each(function (model) {
                this.$('ol').append(this.addSubview(new ContentItem({model: model})).render().el);
            }, this);
            this.showNext();
        }

    });

    // Content Item
    // -----------------------
    ContentItem = Ghost.View.extend({

        tagName: 'li',

        events: {
            'click a': 'setActiveItem'
        },

        active: false,

        initialize: function () {
            this.listenTo(Backbone, 'blog:activeItem', this.checkActive);
            this.listenTo(this.model, 'destroy', this.removeItem);
        },

        removeItem: function () {
            var self = this;
            $.when(this.$el.slideUp()).then(function () {
                self.remove();
            });
        },

        // If the current item isn't active, we trigger the event to
        // notify a change in which item we're viewing.
        setActiveItem: function (e) {
            e.preventDefault();
            if (this.active !== true) {
                Backbone.trigger('blog:activeItem', this.model.id);
                this.render();
            }
        },

        // Checks whether this item is active and doesn't match the current id.
        checkActive: function (id) {
            if (this.model.id !== id) {
                if (this.active) {
                    this.active = false;
                    this.$el.removeClass('active');
                    this.render();
                }
            } else {
                this.active = true;
                this.$el.addClass('active');
            }
        },

        showPreview: function (e) {
            var item = $(e.currentTarget);
            this.$('.content-list-content li').removeClass('active');
            item.addClass('active');
            Backbone.trigger('blog:activeItem', item.data('id'));
        },

        templateName: "list-item",

        templateData: function () {
            return _.extend({active: this.active}, this.model.toJSON());
        }
    });

    // Content preview
    // ----------------
    PreviewContainer = Ghost.View.extend({

        activeId: null,

        events: {
            'click .post-controls .delete' : 'deletePost',
            'click .post-controls .post-edit' : 'editPost'
        },

        initialize: function (options) {
            this.listenTo(Backbone, 'blog:activeItem', this.setActivePreview);
        },

        setActivePreview: function (id) {
            if (this.activeId !== id) {
                this.activeId = id;
                this.render();
            }
        },

        deletePost: function (e) {
            e.preventDefault();
            var self = this;
            this.addSubview(new Ghost.Views.Modal({
                model: {
                    options: {
                        close: false,
                        confirm: {
                            accept: {
                                func: function () {
                                    self.model.destroy({
                                        wait: true
                                    }).then(function () {
                                        Ghost.notifications.addItem({
                                            type: 'success',
                                            message: 'Your post has been deleted.',
                                            status: 'passive'
                                        });
                                    }, function () {
                                        Ghost.notifications.addItem({
                                            type: 'error',
                                            message: 'Your post could not be deleted. Please try again.',
                                            status: 'passive'
                                        });
                                    });
                                },
                                text: "Yes"
                            },
                            reject: {
                                func: function () {
                                    return true;
                                },
                                text: "No"
                            }
                        },
                        type: "action",
                        style: ["wide", "centered"],
                        animation: 'fade'
                    },
                    content: {
                        template: 'blank',
                        title: 'Are you sure you want to delete this post?'
                    }
                }
            }));
        },

        editPost: function (e) {
            e.preventDefault();
            // for now this will disable "open in new tab", but when we have a Router implemented
            // it can go back to being a normal link to '#/ghost/editor/X'
            window.location = '/ghost/editor/' + this.model.get('id');
        },

        templateName: "preview",

        render: function () {
            if (this.activeId) {
                this.model = this.collection.get(this.activeId);
                this.$el.html(this.template(this.templateData()));
            }
            this.$('.content-preview-content').scrollClass({target: '.content-preview', offset: 10});
            this.$('.wrapper').on('click', 'a', function (e) {
                $(e.currentTarget).attr('target', '_blank');
            });
            Ghost.temporary.initToggles(this.$el);
            return this;
        }

    });

}());


// # Ghost Module
// Defines core methods required to build the application

// Module dependencies
var config = require('./../config'),
    when = require('when'),
    express = require('express'),
    errors = require('./server/errorHandling'),
    fs = require('fs'),
    path = require('path'),
    hbs = require('express-hbs'),
    nodefn = require('when/node/function'),
    _ = require('underscore'),
    Polyglot = require('node-polyglot'),
    Mailer = require('./server/mail'),
    models = require('./server/models'),
    plugins = require('./server/plugins'),
    requireTree = require('./server/require-tree'),
    permissions = require('./server/permissions'),
    uuid = require('node-uuid'),

// Variables
    appRoot = path.resolve(__dirname, '../'),
    themePath = path.resolve(appRoot + '/content/themes'),
    pluginPath = path.resolve(appRoot + '/content/plugins'),
    themeDirectories = requireTree(themePath),
    pluginDirectories = requireTree(pluginPath),

    Ghost,
    instance,
    defaults,
    statuses;

// ## Default values
/**
 * A hash of default values to use instead of 'magic' numbers/strings.
 * @type {Object}
 */
defaults = {
    filterPriority: 5,
    maxPriority: 9
};

// ## Article Statuses
/**
 * A list of article status types
 * @type {Object}
 */
statuses = {
    'draft': 'draft',
    'complete': 'complete',
    'approved': 'approved',
    'scheduled': 'scheduled',
    'published': 'published'
};

// ## Module Methods
/**
 * @method Ghost
 * @returns {*}
 * @constructor
 */
Ghost = function () {
    var app,
        polyglot;

    if (!instance) {
        instance = this;

        // Holds the filters
        instance.filterCallbacks = [];

        // Holds the filter hooks (that are built in to Ghost Core)
        instance.filters = [];

        // Holds the theme directories temporarily
        instance.themeDirectories = {};

        // Holds the plugin directories temporarily
        instance.pluginDirectories = {};

        // Holds the persistent notifications
        instance.notifications = [];

        // Holds the available plugins
        instance.availablePlugins = {};

        // Holds the dbhash (mainly used for cookie secret)
        instance.dbHash = undefined;

        app = express();
        polyglot = new Polyglot();

        _.extend(instance, {
            app: function () { return app; },
            config: function () { return config; },

            // there's no management here to be sure this has loaded
            settings: function () { return instance.settingsCache; },
            dataProvider: models,
            blogGlobals:  function () {
                /* this is a bit of a hack until we have a better way to combine settings and config
                 * this data is what becomes globally available to themes */
                return {
                    url: instance.config().env[process.env.NODE_ENV].url,
                    title: instance.settings().title,
                    description: instance.settings().description,
                    logo: instance.settings().logo,
                    cover: instance.settings().cover
                };
            },
            statuses: function () { return statuses; },
            polyglot: function () { return polyglot; },
            mail: new Mailer(),
            getPaths: function () {
                return when.all([themeDirectories, pluginDirectories]).then(function (paths) {
                    instance.themeDirectories = paths[0];
                    instance.pluginDirectories = paths[1];
                    return;
                });
            },
            paths: function () {
                return {
                    'appRoot':          appRoot,
                    'themePath':        themePath,
                    'pluginPath':       pluginPath,
                    'activeTheme':      path.join(themePath, !instance.settingsCache ? "" : instance.settingsCache.activeTheme),
                    'adminViews':       path.join(appRoot, '/core/server/views/'),
                    'helperTemplates':  path.join(appRoot, '/core/server/helpers/tpl/'),
                    'lang':             path.join(appRoot, '/core/shared/lang/'),
                    'availableThemes':  instance.themeDirectories,
                    'availablePlugins': instance.pluginDirectories
                };
            }
        });
    }
    return instance;
};

// Initialise the application
Ghost.prototype.init = function () {
    var self = this;

    return when.join(
        instance.dataProvider.init(),
        instance.getPaths(),
        instance.mail.init(self)
    ).then(function () {
        return models.Settings.populateDefaults();
    }).then(function () {
        // Initialize the settings cache
        return self.updateSettingsCache();
    }).then(function () {
        return self.initPlugins();
    }).then(function () {
        // Initialize the permissions actions and objects
        return permissions.init();
    }).then(function () {
        // get the settings and whatnot
        return when(models.Settings.read('dbHash')).then(function (dbhash) {
            // we already ran this, chill
            self.dbHash = dbhash.attributes.value;
            return dbhash.attributes.value;
        }).otherwise(function (error) {
            // this is where all the "first run" functionality should go
            var dbhash = uuid.v4();
            return when(models.Settings.add({key: 'dbHash', value: dbhash})).then(function (returned) {
                self.dbHash = dbhash;
                return dbhash;
            });
        });
    }, errors.logAndThrowError);
};

// Maintain the internal cache of the settings object
Ghost.prototype.updateSettingsCache = function (settings) {
    var self = this;

    settings = settings || {};

    if (!_.isEmpty(settings)) {
        self.settingsCache = settings;
    } else {
        // TODO: this should use api.browse
        return models.Settings.findAll().then(function (result) {
            var settings = {};
            _.map(result.models, function (member) {
                if (!settings.hasOwnProperty(member.attributes.key)) {
                    if (member.attributes.key === 'activeTheme') {
                        member.attributes.value = member.attributes.value.substring(member.attributes.value.lastIndexOf('/') + 1);
                        var settingsThemePath = path.join(themePath, member.attributes.value);
                        fs.exists(settingsThemePath, function (exists) {
                            if (!exists) {
                                member.attributes.value = "casper";
                            }
                            settings[member.attributes.key] = member.attributes.value;
                        });
                        return;
                    }
                    settings[member.attributes.key] = member.attributes.value;
                }
            });

            self.settingsCache = settings;
        }, errors.logAndThrowError);
    }
};

// ## Template utils

// Compile a template for a handlebars helper
Ghost.prototype.compileTemplate = function (templatePath) {
    return nodefn.call(fs.readFile, templatePath).then(function (templateContents) {
        return hbs.handlebars.compile(templateContents.toString());
    }, errors.logAndThrowError);
};

// Load a template for a handlebars helper
Ghost.prototype.loadTemplate = function (name) {
    var self = this,
        templateFileName = name + '.hbs',
        // Check for theme specific version first
        templatePath = path.join(this.paths().activeTheme, "partials", templateFileName),
        deferred = when.defer();

    // Can't use nodefn here because exists just returns one parameter, true or false

    fs.exists(templatePath, function (exists) {
        if (!exists) {
            // Fall back to helpers templates location
            templatePath = path.join(self.paths().helperTemplates, templateFileName);
        }

        self.compileTemplate(templatePath).then(deferred.resolve, deferred.reject);
    });

    return deferred.promise;
};

// Register a handlebars helper for themes
Ghost.prototype.registerThemeHelper = function (name, fn) {
    hbs.registerHelper(name, fn);
};

// Register a new filter callback function
Ghost.prototype.registerFilter = function (name, priority, fn) {
    // Curry the priority optional parameter to a default of 5
    if (_.isFunction(priority)) {
        fn = priority;
        priority = defaults.filterPriority;
    }

    this.filterCallbacks[name] = this.filterCallbacks[name] || {};
    this.filterCallbacks[name][priority] = this.filterCallbacks[name][priority] || [];

    this.filterCallbacks[name][priority].push(fn);
};

// Unregister a filter callback function
Ghost.prototype.unregisterFilter = function (name, priority, fn) {
    // Curry the priority optional parameter to a default of 5
    if (_.isFunction(priority)) {
        fn = priority;
        priority = defaults.filterPriority;
    }

    // Check if it even exists
    if (this.filterCallbacks[name] && this.filterCallbacks[name][priority]) {
        // Remove the function from the list of filter funcs
        this.filterCallbacks[name][priority] = _.without(this.filterCallbacks[name][priority], fn);
    }
};

// Execute filter functions in priority order
Ghost.prototype.doFilter = function (name, args, callback) {
    var callbacks = this.filterCallbacks[name];

    // Bug out early if no callbacks by that name
    if (!callbacks) {
        return callback(args);
    }

    _.times(defaults.maxPriority + 1, function (priority) {
        // Bug out if no handlers on this priority
        if (!_.isArray(callbacks[priority])) {
            return;
        }

        // Call each handler for this priority level
        _.each(callbacks[priority], function (filterHandler) {
            try {
                args = filterHandler(args);
            } catch (e) {
                // If a filter causes an error, we log it so that it can be debugged, but do not throw the error
                errors.logError(e);
            }
        });
    });

    callback(args);
};

// Initialise plugins.  Will load from config.activePlugins by default
Ghost.prototype.initPlugins = function (pluginsToLoad) {
    pluginsToLoad = pluginsToLoad || models.Settings.activePlugins;

    var self = this;

    return plugins.init(this, pluginsToLoad).then(function (loadedPlugins) {
        // Extend the loadedPlugins onto the available plugins
        _.extend(self.availablePlugins, loadedPlugins);
    }, errors.logAndThrowError);
};

// Initialise Theme or admin
Ghost.prototype.initTheme = function (app) {
    var self = this,
        hbsOptions;
    return function initTheme(req, res, next) {
        app.set('view engine', 'hbs');
        // return the correct mime type for woff files
        express['static'].mime.define({'application/font-woff': ['woff']});

        if (!res.isAdmin) {

            // self.globals is a hack til we have a better way of getting combined settings & config
            hbsOptions = {templateOptions: {data: {blog: self.blogGlobals()}}};

            if (!self.themeDirectories.hasOwnProperty(self.settings().activeTheme)) {
                // Throw an error if the theme is not available...
                // TODO: move this to happen on app start
                errors.logAndThrowError('The currently active theme ' + self.settings().activeTheme + ' is missing.');
            } else if (self.themeDirectories[self.settings().activeTheme].hasOwnProperty('partials')) {
                // Check that the theme has a partials directory before trying to use it
                hbsOptions.partialsDir = path.join(self.paths().activeTheme, 'partials');
            }

            app.engine('hbs', hbs.express3(hbsOptions));

            app.set('views', self.paths().activeTheme);
        } else {
            app.engine('hbs', hbs.express3({partialsDir: self.paths().adminViews + 'partials'}));
            app.set('views', self.paths().adminViews);
            app.use('/public', express['static'](path.join(__dirname, '/client/assets')));
            app.use('/public', express['static'](path.join(__dirname, '/client')));
        }
        app.use(express['static'](self.paths().activeTheme));
        app.use('/shared', express['static'](path.join(__dirname, '/shared')));
        app.use('/content/images', express['static'](path.join(__dirname, '/../content/images')));
        next();
    };
};

// TODO: Expose the defaults for other people to see/manipulate as a static value?
// Ghost.defaults = defaults;

module.exports = Ghost;


it("should not touch underscores in code blocks", function () {

var testPhrase = {input: "    foo_bar_baz", output: /^
foo_bar_baz\n<\/code><\/pre>$/},

keep keep keep keep replace replace replace replace replace replace keep keep keep keep keep

// Currently failing - fixing this causes other issues

//    it("should not create italic words between lines", function () {

//        var testPhrase = {input: "foo_bar\nbar_foo", output: /^<p>foo_bar <br \/>\nbar_foo<\/p>$/},

//            processedMarkup = converter.makeHtml(testPhrase.input);

//        processedMarkup.should.match(testPhrase.output);


/*globals casper, __utils__, url */

casper.test.begin("Settings screen is correct", 15, function suite(test) {
    test.filename = "settings_test.png";

    casper.start(url + "ghost/settings", function testTitleAndUrl() {
        test.assertTitle("", "Ghost admin has no title");
        test.assertUrlMatch(/ghost\/settings\/general$/, "Ghost doesn't require login this time");
    }).viewport(1280, 1024);

    casper.then(function testViews() {
        test.assertExists(".wrapper", "Settings main view is present");
        test.assertExists(".settings-sidebar", "Settings sidebar view is present");
        test.assertExists(".settings-menu", "Settings menu is present");
        test.assertExists(".wrapper", "Settings main view is present");
        test.assertExists(".settings-content", "Settings content view is present");
        test.assertEval(function testGeneralIsActive() {
            return document.querySelector('.settings-menu .general').classList.contains('active');
        }, "general tab is marked active");
        test.assertEval(function testContentIsGeneral() {
            return document.querySelector('.settings-content').id === 'general';
        }, "loaded content is general screen");
    });

    // test the user tab
    casper.thenClick('.settings-menu .users');
    casper.waitForSelector('#user', function then() {
        test.assertEval(function testGeneralIsNotActive() {
            return !document.querySelector('.settings-menu .general').classList.contains('active');
        }, "general tab is not marked active");
        test.assertEval(function testUserIsActive() {
            return document.querySelector('.settings-menu .users').classList.contains('active');
        }, "user tab is marked active");
        test.assertEval(function testContentIsUser() {
            return document.querySelector('.settings-content').id === 'user';
        }, "loaded content is user screen");
    }, function onTimeOut() {
        test.fail('User screen failed to load');
    });

    function handleUserRequest(requestData, request) {
        // make sure we only get requests from the user pane
        if (requestData.url.indexOf('settings/') !== -1) {
            casper.test.fail("Saving the user pane triggered another settings pane to save");
        }
    }

    function handleSettingsRequest(requestData, request) {
        // make sure we only get requests from the user pane
        if (requestData.url.indexOf('users/') !== -1) {
            casper.test.fail("Saving a settings pane triggered the user pane to save");
        }
    }

    casper.then(function listenForRequests() {
        casper.on('resource.requested', handleUserRequest);
    });

    casper.thenClick('#user .button-save');
    casper.waitFor(function successNotification() {
        return this.evaluate(function () {
            return document.querySelectorAll('.js-bb-notification section').length > 0;
        });
    }, function doneWaiting() {

    }, function waitTimeout() {
        casper.test.fail("Saving the user pane did not result in a notification");
    });

    casper.then(function checkUserWasSaved() {
        casper.removeListener('resource.requested', handleUserRequest);
    });

    casper.waitForSelector('.notification-success', function onSuccess() {
        test.assert(true, 'Got success notification');
    }, function onTimeout() {
        test.assert(false, 'No success notification :(');
    });

    casper.thenClick('#main-menu .settings a').then(function testOpeningSettingsTwice() {
        casper.on('resource.requested', handleSettingsRequest);
        test.assertEval(function testUserIsActive() {
            return document.querySelector('.settings-menu .general').classList.contains('active');
        }, "general tab is marked active");

    });

    casper.thenClick('#general .button-save').waitFor(function successNotification() {
        return this.evaluate(function () {
            return document.querySelectorAll('.js-bb-notification section').length > 0;
        });
    }, function doneWaiting() {

    }, function waitTimeout() {
        casper.test.fail("Saving the general pane did not result in a notification");
    });

    casper.then(function checkSettingsWereSaved() {
        casper.removeListener('resource.requested', handleSettingsRequest);
    });

    casper.waitForSelector('.notification-success', function onSuccess() {
        test.assert(true, 'Got success notification');
    }, function onTimeout() {
        test.assert(false, 'No success notification :(');
    });

    casper.run(function () {
        casper.removeListener('resource.requested', handleUserRequest);
        casper.removeListener('resource.requested', handleSettingsRequest);
        test.done();
    });
});

casper.test.begin("User settings screen validates email", 6, function suite(test) {
    var email, brokenEmail;

    test.filename = "user_settings_test.png";

    casper.start(url + "ghost/settings/user", function testTitleAndUrl() {
        test.assertTitle("", "Ghost admin has no title");
        test.assertUrlMatch(/ghost\/settings\/user$/, "Ghost doesn't require login this time");
    }).viewport(1280, 1024);

    casper.then(function setEmailToInvalid() {
        email = casper.getElementInfo('#user-email').attributes.value;
        brokenEmail = email.replace('.', '-');

        casper.fillSelectors('.user-details-container', {
            '#user-email': brokenEmail
        }, false);
    });

    casper.thenClick('#user .button-save');

    casper.waitForResource('/users/');

    casper.waitForSelector('.notification-error', function onSuccess() {
        test.assert(true, 'Got error notification');
        test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
    }, function onTimeout() {
        test.assert(false, 'No error notification :(');
    });

    casper.then(function resetEmailToValid() {
        casper.fillSelectors('.user-details-container', {
            '#user-email': email
        }, false);
    });

    casper.thenClick('#user .button-save');

    casper.waitForResource(/users/);

    casper.waitForSelector('.notification-success', function onSuccess() {
        test.assert(true, 'Got success notification');
        test.assertSelectorDoesntHaveText('.notification-success', '[object Object]');
    }, function onTimeout() {
        test.assert(false, 'No success notification :(');
    });

    casper.run(function () {
        test.done();
    });
});


{{#if messages}}
    {{#each messages}}

              {{message}}
              Close

    {{/each}}
{{/if}}


noReleaseCheck: options['no-release-check'] || process.env.METEOR_NO_RELEASE_CHECK,

const _ = require('underscore');

const files = require('../fs/files.js');
const buildmessage = require('../utils/buildmessage.js');
const utils = require('../utils/utils.js');
const runLog = require('./run-log.js');
const release = require('../packaging/release.js');

const Console = require('../console/console.js').Console;

const Proxy = require('./run-proxy.js').Proxy;
const Selenium = require('./run-selenium.js').Selenium;
const AppRunner = require('./run-app.js').AppRunner;
const MongoRunner = require('./run-mongo.js').MongoRunner;
const Updater = require('./run-updater.js').Updater;

class Runner {
  constructor({
    appHost,
    appPort,
    banner,
    disableOplog,
    cordovaRunner,
    mongoUrl,
    onFailure,
    oplogUrl,
    projectContext,
    proxyHost,
    proxyPort,
    quiet,
    rootUrl,
    selenium,
    seleniumBrowser,
    ...optionsForAppRunner
  }) {
    const self = this;
    self.projectContext = projectContext;

    if (typeof proxyPort === 'undefined') {
      throw new Error('no proxyPort?');
    }

    const listenPort = proxyPort;
    const mongoPort = parseInt(listenPort, 10) + 1;
    self.specifiedAppPort = appPort;
    self.regenerateAppPort();

    self.stopped = false;
    self.quiet = quiet;
    self.banner = banner || files.convertToOSPath(
      files.prettyPath(self.projectContext.projectDir)
    );

    if (rootUrl) {
      self.rootUrl = rootUrl;
    } else {
      self.rootUrl = utils.formatUrl({
        protocol: 'http',
        hostname: proxyHost || "localhost",
        port: listenPort,
      });
    }

    self.proxy = new Proxy({
      listenPort,
      listenHost: proxyHost,
      proxyToPort: self.appPort,
      proxyToHost: appHost,
      onFailure
    });

    self.mongoRunner = null;
    if (mongoUrl) {
      oplogUrl = disableOplog ? null : oplogUrl;
    } else {
      self.mongoRunner = new MongoRunner({
        projectLocalDir: self.projectContext.projectLocalDir,
        port: mongoPort,
        onFailure,
        // For testing mongod failover, run with 3 mongod if the env var is
        // set. Note that data is not preserved from one run to the next.
        multiple: !!process.env.METEOR_TEST_MULTIPLE_MONGOD_REPLSET
      });

      mongoUrl = self.mongoRunner.mongoUrl();
      oplogUrl = disableOplog ? null : self.mongoRunner.oplogUrl();
    }

    self.updater = new Updater();

    self.appRunner = new AppRunner({
      ...optionsForAppRunner,
      projectContext: self.projectContext,
      port: self.appPort,
      listenHost: appHost,
      mongoUrl,
      oplogUrl,
      rootUrl: self.rootUrl,
      proxy: self.proxy,
      noRestartBanner: self.quiet,
      cordovaRunner: cordovaRunner
    });

    self.selenium = null;
    if (selenium) {
      self.selenium = new Selenium({
        runner: self,
        browser: seleniumBrowser
      });
    }
  }

  // XXX leave a pidfile and check if we are already running
  start() {
    const self = this;

    self.proxy.start();

    // print the banner only once we've successfully bound the port
    if (! self.quiet && ! self.stopped) {
      runLog.log("[[[[[ " + self.banner + " ]]]]]\n");
      runLog.log("Started proxy.",  { arrow: true });
    }

    var unblockAppRunner = self.appRunner.makeBeforeStartPromise();
    self._startMongoAsync().then(unblockAppRunner);

    if (! self.stopped) {
      self.updater.start();
    }

    if (! self.stopped) {
      buildmessage.enterJob({ title: "starting your app" }, function () {
        self.appRunner.start();
      });
      if (! self.quiet && ! self.stopped) {
        runLog.log("Started your app.",  { arrow: true });
      }
    }

    if (! self.stopped && ! self.quiet) {
      runLog.log("");
      runLog.log("App running at: " + self.rootUrl,  { arrow: true });

      if (process.platform === "win32") {
        runLog.log("   Type Control-C twice to stop.");
        runLog.log("");
      }
    }

    if (self.selenium && ! self.stopped) {
      buildmessage.enterJob({ title: "starting Selenium" }, function () {
        self.selenium.start();
      });
      if (! self.quiet && ! self.stopped) {
        runLog.log("Started Selenium.", { arrow: true });
      }
    }

    // XXX It'd be nice to (cosmetically) handle failure better. Right
    // now we overwrite the "starting foo..." message with the
    // error. It'd be better to overwrite it with "failed to start
    // foo" and then print the error.
  }

  async _startMongoAsync() {
    if (! this.stopped && this.mongoRunner) {
      this.mongoRunner.start();
      if (! this.stopped && ! this.quiet) {
        runLog.log("Started MongoDB.", { arrow: true });
      }
    }
  }

  // Idempotent
  stop() {
    const self = this;
    if (self.stopped) {
      return;
    }

    self.stopped = true;
    self.proxy.stop();
    self.updater.stop();
    self.mongoRunner && self.mongoRunner.stop();
    self.appRunner.stop();
    self.selenium && self.selenium.stop();
    // XXX does calling this 'finish' still make sense now that runLog is a
    // singleton?
    runLog.finish();
  }

  // Call this whenever you want to regenerate the app's port (if it is not
  // explicitly specified by the user).
  //
  // Rationale: if we randomly chose a port that's in use and the app failed to
  // listen on it, we should try a different port when we restart the app!
  regenerateAppPort() {
    const self = this;
    if (self.specifiedAppPort) {
      self.appPort = self.specifiedAppPort;
    } else {
      self.appPort = require('../utils/utils.js').randomPort();
    }
    if (self.proxy) {
      self.proxy.proxyToPort = self.appPort;
    }
    if (self.appRunner) {
      self.appRunner.port = self.appPort;
    }
  }
}

// Run the app and all of its associated processes. Runs (and does not
// return) until an unrecoverable failure happens. Logs to
// stdout. Returns a suggested exit code.
//
// If 'once' is set, run the app process exactly once and pass through
// its exit code. Return an exit code of 255 if the app process was
// killed by a signal and 254 if the app process could not start
// (build failure, invalid program name, database couldn't start, and
// so on).
//
// If the 'once' option is not set, the default, restart the app
// process if it crashes or if source files change. (Non-app
// processes, such as the database, are always restarted as
// necessary.) The function will only return if there is an
// unrecoverable error, which generally means an error that could not
// be fixed by source code changes (such as the database refusing to
// run), but also currently includes Meteor version mismatches. So the
// exit code will always be 254 because in all other cases we'll
// persevere.
//
// Options:
//
// - proxyPort: the port to connect to to access the application (we will
//   run a proxy here that proxies to the actual app process). required
// - buildOptions: 'buildOptions' argument to bundler.bundle()
// - settingsFile: path to file containing deploy-time settings
// - once: see above
// - banner: replace the application path that is normally printed on
//   startup with an arbitrary string (eg, 'Tests')
// - rootUrl: tell the app that traffic at this URL will be routed to
//   it at '/' (used by the app to construct absolute URLs)
// - disableOplog: don't use oplog tailing
// - mongoUrl: don't start a mongo process; instead use the mongo at
//   this mongo URL
// - oplogUrl: URL of the mongo oplog to use. if mongoUrl isn't
//   set (we're starting a mongo) a default will be provided, but can
//   be overridden. if mongoUrl is set, you must set this or you don't
//   get oplog tailing.
// - recordPackageUsage: (default true) if set to false, don't send
//   information about packages used by this app to the package stats
//   server.
exports.run = function (options) {
  var runOptions = _.clone(options);
  var once = runOptions.once;
  delete runOptions.once;

  var promise = new Promise(function (resolve) {
    runOptions.onFailure = function () {
      // Ensure that runner stops now. You might think this is unnecessary
      // because the runner is stopped immediately after promise.await(), but if
      // the failure happens while runner.start() is still running, we want the
      // rest of start to stop, and it's not like resolve() magically makes
      // us jump to a promise.await() that hasn't happened yet!.
      runner.stop();
      resolve({ outcome: 'failure' });
    };

    runOptions.onRunEnd = function (result) {
      if (once ||
          result.outcome === "conflicting-versions" ||
          result.outcome === "wrong-release" ||
          result.outcome === "outdated-cordova-platforms" ||
          result.outcome === "outdated-cordova-plugins" ||
          (result.outcome === "terminated" &&
           result.signal === undefined && result.code === undefined)) {
        resolve(result);
        return false;  // stop restarting
      }
      runner.regenerateAppPort();
      return true;  // restart it
    };
  });

  runOptions.watchForChanges = ! once;
  runOptions.quiet = false;

  // Ensure process.env.NODE_ENV matches the build mode, with the following precedence:
  // 1. Passed in build mode (if development or production)
  // 2. Existing process.env.NODE_ENV (if it's valid)
  // 3. Default to development (in both cases) otherwise

  // NOTE: because this code only runs when using `meteor run` or `meteor test[-packages`,
  // We *don't* end up defaulting NODE_ENV in this way when bundling/deploying.
  // In those cases, it will default to "production" in packages/meteor/*_env.js

  // We *override* NODE_ENV if build mode is one of these values
  let buildMode = runOptions.buildOptions.buildMode;
  if (buildMode === "development" || buildMode === "production") {
    process.env.NODE_ENV = buildMode;
  }

  let nodeEnv = process.env.NODE_ENV;
  // We *never* override buildMode (it can be "test")
  if (!buildMode) {
    if (nodeEnv === "development" || nodeEnv === "production") {
      runOptions.buildOptions.buildMode = nodeEnv;
    } else {
      runOptions.buildOptions.buildMode = "development";
    }
  }

  if (!nodeEnv) {
    process.env.NODE_ENV = "development";
  }

  var runner = new Runner(runOptions);
  runner.start();
  var result = promise.await();
  runner.stop();

  if (result.outcome === "conflicting-versions") {
    Console.error(
      "The constraint solver could not find a set of package versions to",
      "use that would satisfy the constraints of .meteor/versions and",
      ".meteor/packages. This could be caused by conflicts in",
      ".meteor/versions, conflicts in .meteor/packages, and/or",
      "inconsistent changes to the dependencies in local packages.");
    return 254;
  }

  if (result.outcome === "outdated-cordova-plugins") {
    Console.error("Your app's Cordova plugins have changed.");
    Console.error("Restart meteor to use the new set of plugins.");
    return 254;
  }

  if (result.outcome === "outdated-cordova-platforms") {
    Console.error("Your app's platforms have changed.");
    Console.error("Restart meteor to use the new set of platforms.");
    return 254;
  }

  if (result.outcome === "wrong-release") {
    if (once) {
      // We lost a race where the user ran 'meteor update' and 'meteor
      // run --once' simultaneously.
      throw new Error("wrong release?");
    }

    // If the user did not specify a --release on the command line,
    // and simultaneously runs `meteor update` during this run, just
    // exit and let them restart the run. (We can do something fancy
    // like allowing this to work if the tools version didn't change,
    // or even springboarding if the tools version does change, but
    // this (which prevents weird errors) is a start.)
    var from = release.current.getDisplayName();
    var to = result.displayReleaseNeeded;
    Console.error(
      "Your app has been updated to " + to + " from " + from + ".",
      "Restart meteor to use the new release.");
    return 254;
  }

  if (result.outcome === "failure" ||
      (result.outcome === "terminated" &&
       result.signal === undefined && result.code === undefined)) {
    // Fatal problem with something other than the app process. An
    // explanation should already have been logged.
    return 254;
  }

  if (once && result.outcome === "bundle-fail") {
    Console.arrowError("Build failed:\n\n" +
                       result.errors.formatMessages());
    return 254;
  }

  if (once && result.outcome === "terminated") {
    if (result.signal) {
      Console.error("Killed (" + result.signal + ")");
      return 255;
    } else if (typeof result.code === "number") {
      // We used to print 'Your application is exiting' here, but that
      // seems unnecessarily chatty? once mode is otherwise silent
      return result.code;
    } else {
      // If there is neither a code nor a signal, it means that we
      // failed to start the process. We logged the reason. Probably a
      // bad program name.
      return 254;
    }
  }

  throw new Error("unexpected outcome " + result.outcome);
};