Skip to content
This repository was archived by the owner on Aug 8, 2023. It is now read-only.

Commit 57871ab

Browse files
committed
Add v2.6.0
1 parent ede8304 commit 57871ab

33 files changed

+361
-88
lines changed

core/built/assets/ghost-dark-09a4ad1ce73f34d44589e5e40313318a.css

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/built/assets/ghost-dark-280bfac1d9a1c21f0171d2b7c6c73c89.css

-1
This file was deleted.

core/built/assets/ghost.min-52a4ee573671c227e17e9f9f62bc4d5e.css

-1
This file was deleted.

core/built/assets/ghost.min-6ec029bbb20e60039f2218cb1dfbda7c.css

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/built/assets/ghost.min-a9c5dcce8b8513b4e711b19a2b5fd06d.js core/built/assets/ghost.min-e2b6c53a249cb6882852db3ebfc9829b.js

+30-28
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Loading

core/built/assets/img/disqus.svg

+1
Loading
Loading
Loading
Loading

core/built/assets/img/revue.svg

+1
Loading

core/built/assets/img/typeform.svg

+1
Loading
Loading

core/server/api/shared/http.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const http = (apiImpl) => {
1515
user: req.user,
1616
context: {
1717
api_key_id: (req.api_key && req.api_key.id) ? req.api_key.id : null,
18-
user: ((req.user && req.user.id) || (req.user && models.User.isExternalUser(req.user.id))) ? req.user.id : null
18+
user: ((req.user && req.user.id) || (req.user && models.User.isExternalUser(req.user.id))) ? req.user.id : null,
19+
member: (req.member || null)
1920
}
2021
});
2122

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const _ = require('lodash');
2+
const utils = require('../../../schema/fixtures/utils');
3+
const permissions = require('../../../../services/permissions');
4+
const logging = require('../../../../lib/common/logging');
5+
6+
const resources = ['webhook'];
7+
const _private = {};
8+
9+
_private.getRelations = function getRelations(resource) {
10+
return utils.findPermissionRelationsForObject(resource);
11+
};
12+
13+
_private.printResult = function printResult(result, message) {
14+
if (result.done === result.expected) {
15+
logging.info(message);
16+
} else {
17+
logging.warn(`(${result.done}/${result.expected}) ${message}`);
18+
}
19+
};
20+
21+
module.exports.config = {
22+
transaction: true
23+
};
24+
25+
module.exports.up = (options) => {
26+
const localOptions = _.merge({
27+
context: {internal: true}
28+
}, options);
29+
30+
return Promise.map(resources, (resource) => {
31+
const relationToAdd = _private.getRelations(resource);
32+
33+
return utils.addFixturesForRelation(relationToAdd, localOptions)
34+
.then(result => _private.printResult(result, `Adding permissions_roles fixtures for ${resource}s`))
35+
.then(() => permissions.init(localOptions));
36+
});
37+
};

core/server/lib/image/manipulator.js

+23-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const Promise = require('bluebird');
22
const common = require('../common');
3+
const fs = require('fs-extra');
34

45
/**
56
* @NOTE: Sharp cannot operate on the same image path, that's why we have to use in & out paths.
@@ -8,15 +9,10 @@ const common = require('../common');
89
* https://github.com/lovell/sharp/issues/1360.
910
*/
1011
const process = (options = {}) => {
11-
let sharp, img;
12+
let sharp, img, originalData, originalSize;
1213

1314
try {
1415
sharp = require('sharp');
15-
16-
// @NOTE: workaround for Windows as libvips keeps a reference to the input file
17-
// which makes it impossible to fs.unlink() it on cleanup stage
18-
sharp.cache(false);
19-
img = sharp(options.in);
2016
} catch (err) {
2117
return Promise.reject(new common.errors.InternalServerError({
2218
message: 'Sharp wasn\'t installed',
@@ -25,18 +21,36 @@ const process = (options = {}) => {
2521
}));
2622
}
2723

28-
return img.metadata()
24+
// @NOTE: workaround for Windows as libvips keeps a reference to the input file
25+
// which makes it impossible to fs.unlink() it on cleanup stage
26+
sharp.cache(false);
27+
28+
return fs.readFile(options.in)
29+
.then((data) => {
30+
originalData = data;
31+
32+
// @NOTE: have to use constructor with Buffer for sharp to be able to expose size property
33+
img = sharp(data);
34+
})
35+
.then(() => img.metadata())
2936
.then((metadata) => {
37+
originalSize = metadata.size;
38+
3039
if (metadata.width > options.width) {
3140
img.resize(options.width);
3241
}
3342

3443
// CASE: if you call `rotate` it will automatically remove the orientation (and all other meta data) and rotates
3544
// based on the orientation. It does not rotate if no orientation is set.
3645
img.rotate();
46+
return img.toBuffer({resolveWithObject: true});
3747
})
38-
.then(() => {
39-
return img.toFile(options.out);
48+
.then(({data, info}) => {
49+
if (info.size > originalSize) {
50+
return fs.writeFile(options.out, originalData);
51+
} else {
52+
return fs.writeFile(options.out, data);
53+
}
4054
})
4155
.catch((err) => {
4256
throw new common.errors.InternalServerError({

core/server/models/base/listeners.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ var moment = require('moment-timezone'),
55
sequence = require('../../lib/promise/sequence');
66

77
/**
8+
* @TODO REMOVE WHEN v0.1 IS DROPPED
89
* WHEN access token is created we will update last_seen for user.
910
*/
1011
common.events.on('token.added', function (tokenModel) {
11-
models.User.edit({last_seen: moment().toDate()}, {id: tokenModel.get('user_id')})
12+
models.User.findOne({id: tokenModel.get('user_id')})
13+
.then(function (user) {
14+
return user.updateLastSeen();
15+
})
1216
.catch(function (err) {
1317
common.logging.error(new common.errors.GhostError({err: err, level: 'critical'}));
1418
});

core/server/models/user.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,11 @@ User = ghostBookshelf.Model.extend({
259259
});
260260
},
261261

262+
updateLastSeen: function updateLastSeen() {
263+
this.set({last_seen: new Date()});
264+
return this.save();
265+
},
266+
262267
enforcedFilters: function enforcedFilters(options) {
263268
if (options.context && options.context.internal) {
264269
return null;
@@ -756,7 +761,7 @@ User = ghostBookshelf.Model.extend({
756761
var self = this;
757762

758763
return this.getByEmail(object.email)
759-
.then(function then(user) {
764+
.then((user) => {
760765
if (!user) {
761766
throw new common.errors.NotFoundError({
762767
message: common.i18n.t('errors.models.user.noUserWithEnteredEmailAddr')
@@ -776,12 +781,15 @@ User = ghostBookshelf.Model.extend({
776781
}
777782

778783
return self.isPasswordCorrect({plainPassword: object.password, hashedPassword: user.get('password')})
779-
.then(function then() {
780-
user.set({status: 'active', last_seen: new Date()});
784+
.then(() => {
785+
return user.updateLastSeen();
786+
})
787+
.then(() => {
788+
user.set({status: 'active'});
781789
return user.save();
782790
});
783791
})
784-
.catch(function (err) {
792+
.catch((err) => {
785793
if (err.message === 'NotFound' || err.message === 'EmptyResponse') {
786794
throw new common.errors.NotFoundError({
787795
message: common.i18n.t('errors.models.user.noUserWithEnteredEmailAddr')

core/server/services/auth/authenticate.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const models = require('../../models');
44
const common = require('../../lib/common');
55
const session = require('./session');
66
const apiKeyAuth = require('./api-key');
7+
const members = require('./members');
78

89
const authenticate = {
910
// ### Authenticate Client Middleware
@@ -103,7 +104,7 @@ const authenticate = {
103104

104105
// ### v2 API auth middleware
105106
authenticateAdminAPI: [session.safeGetSession, session.getUser],
106-
authenticateContentApiKey: apiKeyAuth.content.authenticateContentApiKey
107+
authenticateContentApi: [apiKeyAuth.content.authenticateContentApiKey, members.authenticateMembersToken]
107108
};
108109

109110
module.exports = authenticate;

core/server/services/auth/authorize.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,18 @@ const authorize = {
3838
},
3939

4040
authorizeAdminAPI: [session.ensureUser],
41-
// used by API v2 endpoints
41+
authorizeContentApi(req, res, next) {
42+
const hasApiKey = req.api_key && req.api_key.id;
43+
const hasMember = req.member;
44+
if (hasApiKey) {
45+
return next();
46+
}
47+
if (labs.isSet('members') && hasMember) {
48+
return next();
49+
}
50+
return next(new common.errors.NoPermissionError({message: common.i18n.t('errors.middleware.auth.pleaseSignInOrAuthenticate')}));
51+
},
52+
4253
requiresAuthorizedUserOrApiKey(req, res, next) {
4354
const hasUser = req.user && req.user.id;
4455
const hasApiKey = req.api_key && req.api_key.id;
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const jwt = require('jsonwebtoken');
2+
const common = require('../../../lib/common');
3+
4+
const authenticateMembersToken = (req, res, next) => {
5+
if (!req.get('authorization')) {
6+
return next();
7+
}
8+
9+
const [scheme, credentials] = req.get('authorization').split(/\s+/);
10+
11+
if (scheme !== 'GhostMembers') {
12+
return next();
13+
}
14+
15+
return jwt.verify(credentials, null, {
16+
algorithms: ['none']
17+
}, function (err, claims) {
18+
if (err) {
19+
return next(new common.errors.UnauthorizedError({err}));
20+
}
21+
req.member = claims;
22+
return next();
23+
});
24+
};
25+
26+
module.exports = {
27+
authenticateMembersToken
28+
};

core/server/services/labs.js

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
var settingsCache = require('./settings/cache'),
2-
_ = require('lodash'),
3-
Promise = require('bluebird'),
4-
SafeString = require('./themes/engine').SafeString,
5-
common = require('../lib/common'),
6-
labs = module.exports = {};
1+
const settingsCache = require('./settings/cache');
2+
const _ = require('lodash');
3+
const Promise = require('bluebird');
4+
const SafeString = require('./themes/engine').SafeString;
5+
const common = require('../lib/common');
6+
const config = require('../config');
7+
let labs = module.exports = {};
78

89
labs.isSet = function isSet(flag) {
10+
/**
11+
* TODO: Uses hard-check for members prototype, removed here when added to settings
12+
*/
13+
if (flag === 'members' && config.get('enableDeveloperExperiments')) {
14+
return true;
15+
}
916
var labsConfig = settingsCache.get('labs');
1017
return labsConfig && labsConfig[flag] && labsConfig[flag] === true;
1118
};

core/server/services/settings/validate.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ _private.validateData = function validateData(object) {
7777
type: ['read', 'browse'],
7878
resource: _.map(RESOURCE_CONFIG.QUERY, 'resource')
7979
};
80-
const allowedQueryOptions = ['limit', 'filter', 'include', 'slug', 'visibility', 'status'];
80+
const allowedQueryOptions = ['limit', 'order', 'filter', 'include', 'slug', 'visibility', 'status'];
8181
const allowedRouterOptions = ['redirect', 'slug'];
8282
const defaultRouterOptions = {
8383
redirect: true

core/server/services/slack.js

+59-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ var common = require('../lib/common'),
44
urlService = require('../services/url'),
55
settingsCache = require('./settings/cache'),
66
schema = require('../data/schema').checks,
7+
moment = require('moment'),
8+
79
defaultPostSlugs = [
810
'welcome',
911
'the-editor',
@@ -22,13 +24,18 @@ function getSlackSettings() {
2224
}
2325

2426
function ping(post) {
25-
var message,
27+
let message,
28+
title,
29+
author,
2630
slackData = {},
27-
slackSettings = getSlackSettings();
31+
slackSettings = getSlackSettings(),
32+
blogTitle = settingsCache.get('title');
2833

2934
// If this is a post, we want to send the link of the post
3035
if (schema.isPost(post)) {
3136
message = urlService.getUrlByResourceId(post.id, {absolute: true});
37+
title = post.title ? post.title : null;
38+
author = post.authors ? post.authors[0] : null;
3239
} else {
3340
message = post.message;
3441
}
@@ -48,12 +55,56 @@ function ping(post) {
4855
return;
4956
}
5057

51-
slackData = {
52-
text: message,
53-
unfurl_links: true,
54-
icon_url: imageLib.blogIcon.getIconUrl(true),
55-
username: 'Ghost'
56-
};
58+
if (schema.isPost(post)) {
59+
slackData = {
60+
// We are handling the case of test notification here by checking
61+
// if it is a post or a test message to check webhook working.
62+
text: `Notification from *${blogTitle}* :ghost:`,
63+
unfurl_links: true,
64+
icon_url: imageLib.blogIcon.getIconUrl(true),
65+
username: 'Ghost',
66+
// We don't want to send attachment if it is a test notification.
67+
attachments: [
68+
{
69+
fallback: 'Sorry, content cannot be shown.',
70+
title: title,
71+
title_link: message,
72+
author_name: blogTitle,
73+
image_url: post ? urlService.utils.urlFor('image', {image: post.feature_image}, true) : null,
74+
color: '#008952',
75+
fields: [
76+
{
77+
title: 'Description',
78+
value: post.custom_excerpt ? post.custom_excerpt : `${post.html.replace(/<[^>]+>/g, '').split('.').slice(0, 3).join('.')}.`,
79+
short: false
80+
}
81+
]
82+
},
83+
{
84+
fallback: 'Sorry, content cannot be shown.',
85+
color: '#008952',
86+
thumb_url: author ? urlService.utils.urlFor('image', {image: author.profile_image}, true) : null,
87+
fields: [
88+
{
89+
title: 'Author',
90+
value: author ? `<${urlService.getUrlByResourceId(author.id, {absolute: true})} | ${author.name}>` : null,
91+
short: true
92+
}
93+
],
94+
footer: blogTitle,
95+
footer_icon: imageLib.blogIcon.getIconUrl(true),
96+
ts: moment().unix()
97+
}
98+
]
99+
};
100+
} else {
101+
slackData = {
102+
text: message,
103+
unfurl_links: true,
104+
icon_url: imageLib.blogIcon.getIconUrl(true),
105+
username: 'Ghost'
106+
};
107+
}
57108

58109
return request(slackSettings.url, {
59110
body: JSON.stringify(slackData),

core/server/services/themes/middleware.js

+9
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ themeMiddleware.updateTemplateData = function updateTemplateData(req, res, next)
6060
labsData = _.cloneDeep(settingsCache.get('labs')),
6161
themeData = {};
6262

63+
/**
64+
* TODO: Uses hard-check for members prototype, removed here when added to settings
65+
*/
66+
if (config.get('enableDeveloperExperiments')) {
67+
Object.assign(labsData, {
68+
members: true
69+
});
70+
}
71+
6372
if (activeTheme.get()) {
6473
themeData.posts_per_page = activeTheme.get().config('posts_per_page');
6574
}

0 commit comments

Comments
 (0)