0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-03 23:00:14 -05:00

Merge branch '0.4-maintenance'

This commit is contained in:
Hannah Wolfe 2014-01-28 09:25:38 +00:00
commit be9afc439c
17 changed files with 116 additions and 57 deletions

View file

@ -55,7 +55,7 @@
1. Login 1. Login
============================================================================= */ ============================================================================= */
#login { .login-form {
@include box-sizing(border-box); @include box-sizing(border-box);
max-width: 530px; max-width: 530px;
color: lighten($midgrey, 15%); color: lighten($midgrey, 15%);
@ -182,7 +182,7 @@
2. Signup and Reset 2. Signup and Reset
============================================================================= */ ============================================================================= */
#signup, #reset { .signup-form, .reset-form {
@include box-sizing(border-box); @include box-sizing(border-box);
max-width: 280px; max-width: 280px;
color: lighten($midgrey, 15%); color: lighten($midgrey, 15%);
@ -270,7 +270,7 @@
3. Forgotten 3. Forgotten
============================================================================= */ ============================================================================= */
#forgotten { .forgotten-form {
@include box-sizing(border-box); @include box-sizing(border-box);
max-width: 280px; max-width: 280px;
color: lighten($midgrey, 15%); color: lighten($midgrey, 15%);

View file

@ -18,7 +18,7 @@
.editor { .editor {
#notifications { .notifications {
@include breakpoint($biggerthan-mobile) { @include breakpoint($biggerthan-mobile) {
bottom: 40px; bottom: 40px;
} }
@ -376,7 +376,7 @@
body.zen { body.zen {
background: lighten($lightbrown, 3%); background: lighten($lightbrown, 3%);
#usermenu {display: none;} .usermenu {display: none;}
#global-header, #publish-bar { #global-header, #publish-bar {
opacity: 0; opacity: 0;
height: 0; height: 0;

View file

@ -540,7 +540,7 @@ nav {
}//.navbar }//.navbar
// The user menu in the top right corner of the screen // The user menu in the top right corner of the screen
#usermenu { .usermenu.subnav {
position:absolute; position:absolute;
top:0; top:0;
right:0; right:0;
@ -636,7 +636,7 @@ nav {
} }
#usermenu { .usermenu {
position:fixed; position:fixed;
top:0; top:0;
right:auto; right:auto;
@ -928,7 +928,7 @@ nav {
Notifications Notifications
========================================================================== */ ========================================================================== */
#notifications { .notifications {
@include breakpoint($biggerthan-mobile) { @include breakpoint($biggerthan-mobile) {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@ -1028,6 +1028,12 @@ nav {
background: $blue; background: $blue;
} }
// Hide extra space taken up by update notification
.update-available main {
bottom: 56px;
}
/* ========================================================================== /* ==========================================================================
Modals Modals
========================================================================== */ ========================================================================== */

View file

@ -1,4 +1,4 @@
<form id="forgotten" method="post" novalidate="novalidate"> <form id="forgotten" class="forgotten-form" method="post" novalidate="novalidate">
<div class="email-wrap"> <div class="email-wrap">
<input class="email" type="email" placeholder="Email Address" name="email" autocapitalize="off" autocorrect="off"> <input class="email" type="email" placeholder="Email Address" name="email" autocapitalize="off" autocorrect="off">
</div> </div>

View file

@ -1,4 +1,4 @@
<form id="login" method="post" novalidate="novalidate"> <form id="login" class="login-form" method="post" novalidate="novalidate">
<div class="email-wrap"> <div class="email-wrap">
<input class="email" type="email" placeholder="Email Address" name="email" autocapitalize="off" autocorrect="off"> <input class="email" type="email" placeholder="Email Address" name="email" autocapitalize="off" autocorrect="off">
</div> </div>

View file

@ -1,4 +1,4 @@
<form id="reset" method="post" novalidate="novalidate"> <form id="reset" class="reset-form" method="post" novalidate="novalidate">
<div class="password-wrap"> <div class="password-wrap">
<input class="password" type="password" placeholder="Password" name="newpassword" /> <input class="password" type="password" placeholder="Password" name="newpassword" />
</div> </div>

View file

@ -1,4 +1,4 @@
<form id="signup" method="post" novalidate="novalidate"> <form id="signup" class="signup-form" method="post" novalidate="novalidate">
<div class="name-wrap"> <div class="name-wrap">
<input class="name" type="text" placeholder="Full Name" name="name" autocorrect="off" /> <input class="name" type="text" placeholder="Full Name" name="name" autocorrect="off" />
</div> </div>

View file

@ -5,12 +5,17 @@
Ghost.Views.Debug = Ghost.View.extend({ Ghost.Views.Debug = Ghost.View.extend({
events: { events: {
"click .settings-menu a": "handleMenuClick", "click .settings-menu a": "handleMenuClick",
"click #startupload": "handleUploadClick",
"click .js-delete": "handleDeleteClick" "click .js-delete": "handleDeleteClick"
}, },
initialize: function () { initialize: function () {
var view = this;
this.uploadButton = this.$el.find('#startupload');
// Disable import button and initizalize BlueImp file upload // Disable import button and initizalize BlueImp file upload
$('#startupload').prop('disabled', true); this.uploadButton.prop('disabled', 'disabled');
$('#importfile').fileupload({ $('#importfile').fileupload({
url: Ghost.paths.apiRoot + '/db/', url: Ghost.paths.apiRoot + '/db/',
limitMultiFileUploads: 1, limitMultiFileUploads: 1,
@ -21,16 +26,12 @@
dataType: 'json', dataType: 'json',
add: function (e, data) { add: function (e, data) {
/*jslint unparam:true*/ /*jslint unparam:true*/
// unregister click event to preveng duplicate binding
$('#startupload').off("click"); // Bind the upload data to the view, so it is
data.context = $('#startupload').prop('disabled', false) // available to the click handler, and enable the
.click(function () { // upload button.
$('#startupload').prop('disabled', true); view.fileUploadData = data;
data.context = $('#startupload').text('Importing'); data.context = view.uploadButton.removeProp('disabled');
data.submit();
// unregister click event to allow different subsequent uploads
$('#startupload').off('click');
});
}, },
done: function (e, data) { done: function (e, data) {
/*jslint unparam:true*/ /*jslint unparam:true*/
@ -77,6 +78,18 @@
return false; return false;
}, },
handleUploadClick: function (ev) {
ev.preventDefault();
if (!this.uploadButton.prop('disabled')) {
this.fileUploadData.context = this.uploadButton.text('Importing');
this.fileUploadData.submit();
}
// Prevent double post by disabling the button.
this.uploadButton.prop('disabled', 'disabled');
},
handleDeleteClick: function (ev) { handleDeleteClick: function (ev) {
ev.preventDefault(); ev.preventDefault();
this.addSubview(new Ghost.Views.Modal({ this.addSubview(new Ghost.Views.Modal({
@ -141,4 +154,4 @@
})); }));
} }
}); });
}()); }());

View file

@ -64,6 +64,7 @@
Ghost.Views.Signup = Ghost.View.extend({ Ghost.Views.Signup = Ghost.View.extend({
initialize: function () { initialize: function () {
this.submitted = "no";
this.render(); this.render();
}, },
@ -95,10 +96,12 @@
Ghost.Validate.check(name, "Please enter a name").len(1); Ghost.Validate.check(name, "Please enter a name").len(1);
Ghost.Validate.check(email, "Please enter a correct email address").isEmail(); Ghost.Validate.check(email, "Please enter a correct email address").isEmail();
Ghost.Validate.check(password, "Your password is not long enough. It must be at least 8 characters long.").len(8); Ghost.Validate.check(password, "Your password is not long enough. It must be at least 8 characters long.").len(8);
Ghost.Validate.check(this.submitted, "Ghost is signing you up. Please wait...").equals("no");
if (Ghost.Validate._errors.length > 0) { if (Ghost.Validate._errors.length > 0) {
Ghost.Validate.handleErrors(); Ghost.Validate.handleErrors();
} else { } else {
this.submitted = "yes";
$.ajax({ $.ajax({
url: Ghost.paths.subdir + '/ghost/signup/', url: Ghost.paths.subdir + '/ghost/signup/',
type: 'POST', type: 'POST',
@ -114,6 +117,7 @@
window.location.href = msg.redirect; window.location.href = msg.redirect;
}, },
error: function (xhr) { error: function (xhr) {
this.submitted = "no";
Ghost.notifications.clearEverything(); Ghost.notifications.clearEverything();
Ghost.notifications.addItem({ Ghost.notifications.addItem({
type: 'error', type: 'error',

View file

@ -5,6 +5,10 @@
(function () { (function () {
"use strict"; "use strict";
var parseDateFormats = ['DD MMM YY HH:mm', 'DD MMM YYYY HH:mm', 'DD/MM/YY HH:mm', 'DD/MM/YYYY HH:mm',
'DD-MM-YY HH:mm', 'DD-MM-YYYY HH:mm'],
displayDateFormat = 'DD MMM YY @ HH:mm';
Ghost.View.PostSettings = Ghost.View.extend({ Ghost.View.PostSettings = Ghost.View.extend({
events: { events: {
@ -17,11 +21,10 @@
initialize: function () { initialize: function () {
if (this.model) { if (this.model) {
// These three items can be updated outside of the post settings menu, so have to be listened to.
this.listenTo(this.model, 'change:id', this.render); this.listenTo(this.model, 'change:id', this.render);
this.listenTo(this.model, 'change:status', this.render);
this.listenTo(this.model, 'change:published_at', this.render);
this.listenTo(this.model, 'change:page', this.render);
this.listenTo(this.model, 'change:title', this.updateSlugPlaceholder); this.listenTo(this.model, 'change:title', this.updateSlugPlaceholder);
this.listenTo(this.model, 'change:published_at', this.updatePublishedDate);
} }
}, },
@ -29,8 +32,7 @@
var slug = this.model ? this.model.get('slug') : '', var slug = this.model ? this.model.get('slug') : '',
pubDate = this.model ? this.model.get('published_at') : 'Not Published', pubDate = this.model ? this.model.get('published_at') : 'Not Published',
$pubDateEl = this.$('.post-setting-date'), $pubDateEl = this.$('.post-setting-date'),
$postSettingSlugEl = this.$('.post-setting-slug'), $postSettingSlugEl = this.$('.post-setting-slug');
publishedDateFormat = 'DD MMM YY @ HH:mm';
$postSettingSlugEl.val(slug); $postSettingSlugEl.val(slug);
@ -41,10 +43,10 @@
// Insert the published date, and make it editable if it exists. // Insert the published date, and make it editable if it exists.
if (this.model && this.model.get('published_at')) { if (this.model && this.model.get('published_at')) {
pubDate = moment(pubDate).format(publishedDateFormat); pubDate = moment(pubDate).format(displayDateFormat);
$pubDateEl.attr('placeholder', ''); $pubDateEl.attr('placeholder', '');
} else { } else {
$pubDateEl.attr('placeholder', moment().format(publishedDateFormat)); $pubDateEl.attr('placeholder', moment().format(displayDateFormat));
} }
if (this.model && this.model.get('id')) { if (this.model && this.model.get('id')) {
@ -130,6 +132,7 @@
}, },
error : function (model, xhr) { error : function (model, xhr) {
/*jslint unparam:true*/ /*jslint unparam:true*/
slugEl.value = model.previous('slug');
Ghost.notifications.addItem({ Ghost.notifications.addItem({
type: 'error', type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr), message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
@ -139,13 +142,23 @@
}); });
}, 500), }, 500),
updatePublishedDate: function () {
var pubDate = this.model.get('published_at') ? moment(this.model.get('published_at'))
.format(displayDateFormat) : '',
$pubDateEl = this.$('.post-setting-date');
// Only change the date if it's different
if (pubDate && $pubDateEl.val() !== pubDate) {
$pubDateEl.val(pubDate);
}
},
editDate: _.debounce(function (e) { editDate: _.debounce(function (e) {
e.preventDefault(); e.preventDefault();
var self = this, var self = this,
parseDateFormats = ['DD MMM YY HH:mm', 'DD MMM YYYY HH:mm', 'DD/MM/YY HH:mm', 'DD/MM/YYYY HH:mm', 'DD-MM-YY HH:mm', 'DD-MM-YYYY HH:mm'],
displayDateFormat = 'DD MMM YY @ HH:mm',
errMessage = '', errMessage = '',
pubDate = self.model.get('published_at'), pubDate = moment(self.model.get('published_at')).format(displayDateFormat),
pubDateEl = e.currentTarget, pubDateEl = e.currentTarget,
newPubDate = pubDateEl.value, newPubDate = pubDateEl.value,
pubDateMoment, pubDateMoment,
@ -228,6 +241,8 @@
}, },
error : function (model, xhr) { error : function (model, xhr) {
/*jslint unparam:true*/ /*jslint unparam:true*/
// Reset back to original value
pubDateEl.value = pubDateMoment ? pubDateMoment.format(displayDateFormat) : '';
Ghost.notifications.addItem({ Ghost.notifications.addItem({
type: 'error', type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr), message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
@ -266,6 +281,7 @@
}, },
error : function (model, xhr) { error : function (model, xhr) {
/*jslint unparam:true*/ /*jslint unparam:true*/
pageEl.prop('checked', model.previous('page'));
Ghost.notifications.addItem({ Ghost.notifications.addItem({
type: 'error', type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr), message: Ghost.Views.Utils.getRequestErrorMessage(xhr),

View file

@ -562,7 +562,7 @@ coreHelpers.adminUrl = function (options) {
return config.paths.urlFor(context, absolute); return config.paths.urlFor(context, absolute);
}; };
coreHelpers.updateNotification = function () { coreHelpers.updateNotification = function (options) {
var output = ''; var output = '';
if (config().updateCheck === false || !this.currentUser) { if (config().updateCheck === false || !this.currentUser) {
@ -571,9 +571,13 @@ coreHelpers.updateNotification = function () {
return updateCheck.showUpdateNotification().then(function (result) { return updateCheck.showUpdateNotification().then(function (result) {
if (result) { if (result) {
output = '<div class="notification-success">' + if (options && options.hash && options.hash.classOnly) {
'A new version of Ghost is available! Hot damn. ' + output = ' update-available';
'<a href="http://ghost.org/download">Upgrade now</a></div>'; } else {
output = '<div class="notification-success">' +
'A new version of Ghost is available! Hot damn. ' +
'<a href="http://ghost.org/download">Upgrade now</a></div>';
}
} }
return output; return output;

View file

@ -183,11 +183,7 @@ function isSSLrequired(isAdmin) {
// and redirect if needed // and redirect if needed
function checkSSL(req, res, next) { function checkSSL(req, res, next) {
if (isSSLrequired(res.isAdmin)) { if (isSSLrequired(res.isAdmin)) {
// Check if X-Forarded-Proto headers are sent, if they are check for https. if (!req.secure) {
// If they are not assume true to avoid infinite redirect loop.
// If the X-Forwarded-Proto header is missing and Express cannot automatically sense HTTPS the redirect will not be made.
var httpsHeader = req.header('X-Forwarded-Proto') !== undefined ? req.header('X-Forwarded-Proto').toLowerCase() === 'https' ? true : false : true;
if (!req.secure && !httpsHeader) {
return res.redirect(301, url.format({ return res.redirect(301, url.format({
protocol: 'https:', protocol: 'https:',
hostname: url.parse(config().url).hostname, hostname: url.parse(config().url).hostname,
@ -208,6 +204,10 @@ module.exports = function (server, dbHash) {
expressServer = server; expressServer = server;
middleware.cacheServer(expressServer); middleware.cacheServer(expressServer);
// Make sure 'req.secure' is valid for proxied requests
// (X-Forwarded-Proto header will be checked, if present)
expressServer.enable('trust proxy');
// Logging configuration // Logging configuration
if (expressServer.get('env') !== 'development') { if (expressServer.get('env') !== 'development') {
expressServer.use(express.logger()); expressServer.use(express.logger());
@ -226,13 +226,16 @@ module.exports = function (server, dbHash) {
// First determine whether we're serving admin or theme content // First determine whether we're serving admin or theme content
expressServer.use(manageAdminAndTheme); expressServer.use(manageAdminAndTheme);
// Force SSL
expressServer.use(checkSSL);
// Admin only config // Admin only config
expressServer.use(subdir + '/ghost', middleware.whenEnabled('admin', express['static'](path.join(corePath, '/client/assets'), {maxAge: ONE_YEAR_MS}))); expressServer.use(subdir + '/ghost', middleware.whenEnabled('admin', express['static'](path.join(corePath, '/client/assets'), {maxAge: ONE_YEAR_MS})));
// Force SSL
// NOTE: Importantly this is _after_ the check above for admin-theme static resources,
// which do not need HTTPS. In fact, if HTTPS is forced on them, then 404 page might
// not display properly when HTTPS is not available!
expressServer.use(checkSSL);
// Theme only config // Theme only config
expressServer.use(subdir, middleware.whenEnabled(expressServer.get('activeTheme'), middleware.staticTheme())); expressServer.use(subdir, middleware.whenEnabled(expressServer.get('activeTheme'), middleware.staticTheme()));

View file

@ -213,8 +213,13 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
}); });
}; };
slug = base.trim();
// Remove non ascii characters
slug = unidecode(slug);
// Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"` // Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"`
slug = base.trim().replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '') slug = slug.replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '')
// Replace dots and spaces with a dash // Replace dots and spaces with a dash
.replace(/(\s|\.)/g, '-') .replace(/(\s|\.)/g, '-')
// Convert 2 or more dashes into a single dash // Convert 2 or more dashes into a single dash
@ -224,8 +229,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
// Remove trailing hyphen // Remove trailing hyphen
slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug; slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug;
// Remove non ascii characters
slug = unidecode(slug);
// Check the filtered slug doesn't match any of the reserved keywords // Check the filtered slug doesn't match any of the reserved keywords
slug = /^(ghost|ghost\-admin|admin|wp\-admin|wp\-login|dashboard|logout|login|signin|signup|signout|register|archive|archives|category|categories|tag|tags|page|pages|post|posts|user|users|rss)$/g slug = /^(ghost|ghost\-admin|admin|wp\-admin|wp\-login|dashboard|logout|login|signin|signup|signout|register|archive|archives|category|categories|tag|tags|page|pages|post|posts|user|users|rss)$/g
.test(slug) ? slug + '-post' : slug; .test(slug) ? slug + '-post' : slug;

View file

@ -31,7 +31,7 @@
<link rel="stylesheet" href="{{asset "css/screen.css" ghost="true"}}"> <link rel="stylesheet" href="{{asset "css/screen.css" ghost="true"}}">
{{{block "pageStyles"}}} {{{block "pageStyles"}}}
</head> </head>
<body class="{{bodyClass}}"> <body class="{{bodyClass}}{{updateNotification classOnly="true"}}">
{{#unless hideNavbar}} {{#unless hideNavbar}}
{{> navbar}} {{> navbar}}
{{/unless}} {{/unless}}
@ -39,7 +39,7 @@
<main role="main" id="main"> <main role="main" id="main">
{{updateNotification}} {{updateNotification}}
<aside id="notifications"> <aside id="notifications" class="notifications">
{{> notifications}} {{> notifications}}
</aside> </aside>

View file

@ -8,7 +8,7 @@
<li class="{{navClass}}{{#if selected}} active{{/if}}"><a href="{{adminUrl}}{{path}}">{{name}}</a></li> <li class="{{navClass}}{{#if selected}} active{{/if}}"><a href="{{adminUrl}}{{path}}">{{name}}</a></li>
{{/each}} {{/each}}
<li id="usermenu" class="subnav"> <li id="usermenu" class="usermenu subnav">
<a href="#" data-toggle="ul" class="dropdown"> <a href="#" data-toggle="ul" class="dropdown">
<img class="avatar" src="{{#if currentUser.image}}{{currentUser.image}}{{else}}{{asset "shared/img/user-image.png"}}{{/if}}" alt="Avatar" /> <img class="avatar" src="{{#if currentUser.image}}{{currentUser.image}}{{else}}{{asset "shared/img/user-image.png"}}{{/if}}" alt="Avatar" />
<span class="name">{{#if currentUser.name}}{{currentUser.name}}{{else}}{{currentUser.email}}{{/if}}</span> <span class="name">{{#if currentUser.name}}{{currentUser.name}}{{else}}{{currentUser.email}}{{/if}}</span>

View file

@ -940,21 +940,30 @@ describe('Core Helpers', function () {
}); });
describe('updateNotification', function () { describe('updateNotification', function () {
it('outputs a correctly formatted notification when db version is higher than package version', function (done) { it('outputs a correctly formatted notification when db version is higher than package version', function (done) {
var output = '<div class="notification-success">' + var defaultOutput = '<div class="notification-success">' +
'A new version of Ghost is available! Hot damn. ' + 'A new version of Ghost is available! Hot damn. ' +
'<a href="http://ghost.org/download">Upgrade now</a></div>'; '<a href="http://ghost.org/download">Upgrade now</a></div>',
classOutput = ' update-available';
apiStub.restore(); apiStub.restore();
apiStub = sandbox.stub(api.settings, 'read', function () { apiStub = sandbox.stub(api.settings, 'read', function () {
var futureversion = packageInfo.version.split('.'); var futureversion = packageInfo.version.split('.');
futureversion[futureversion.length-1] = parseInt(futureversion[futureversion.length-1], 10) + 1; futureversion[futureversion.length - 1] = parseInt(futureversion[futureversion.length - 1], 10) + 1;
return when({value: futureversion.join('.')}); return when({value: futureversion.join('.')});
}); });
helpers.updateNotification.call({currentUser: {name: 'bob'}}).then(function (rendered) { helpers.updateNotification.call({currentUser: {name: 'bob'}}).then(function (rendered) {
should.exist(rendered); should.exist(rendered);
rendered.should.equal(output); rendered.should.equal(defaultOutput);
// Test classOnly option
return helpers.updateNotification.call({currentUser: {name: 'bob'}}, {'hash': {'classOnly': 'true'}});
}).then(function (rendered) {
should.exist(rendered);
rendered.should.equal(classOutput);
done(); done();
}).then(null, done); }).then(null, done);
}); });

View file

@ -1,6 +1,6 @@
{ {
"name" : "ghost", "name" : "ghost",
"version" : "0.4.0", "version" : "0.4.1-rc1",
"description" : "Just a blogging platform.", "description" : "Just a blogging platform.",
"author" : "Ghost Foundation", "author" : "Ghost Foundation",
"homepage" : "http://ghost.org", "homepage" : "http://ghost.org",