ghost 5.36.1 → 5.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-adapter-cache-memory-ttl-5.38.0.tgz +0 -0
- package/components/tryghost-adapter-cache-redis-5.38.0.tgz +0 -0
- package/components/tryghost-adapter-manager-5.38.0.tgz +0 -0
- package/components/tryghost-api-framework-5.38.0.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.38.0.tgz +0 -0
- package/components/tryghost-audience-feedback-5.38.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.38.0.tgz +0 -0
- package/components/tryghost-constants-5.38.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.38.0.tgz +0 -0
- package/components/tryghost-data-generator-5.38.0.tgz +0 -0
- package/components/tryghost-domain-events-5.38.0.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.38.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.38.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.38.0.tgz +0 -0
- package/components/{tryghost-email-content-generator-5.36.1.tgz → tryghost-email-content-generator-5.38.0.tgz} +0 -0
- package/components/tryghost-email-events-5.38.0.tgz +0 -0
- package/components/tryghost-email-service-5.38.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.38.0.tgz +0 -0
- package/components/tryghost-event-aware-cache-wrapper-5.38.0.tgz +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.36.1.tgz → tryghost-express-dynamic-redirects-5.38.0.tgz} +0 -0
- package/components/tryghost-external-media-inliner-5.38.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.38.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.38.0.tgz +0 -0
- package/components/tryghost-i18n-5.38.0.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.38.0.tgz +0 -0
- package/components/tryghost-importer-revue-5.38.0.tgz +0 -0
- package/components/tryghost-job-manager-5.38.0.tgz +0 -0
- package/components/tryghost-link-redirects-5.38.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.38.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.38.0.tgz +0 -0
- package/components/tryghost-magic-link-5.38.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.38.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.38.0.tgz +0 -0
- package/components/tryghost-member-events-5.38.0.tgz +0 -0
- package/components/tryghost-members-api-5.38.0.tgz +0 -0
- package/components/tryghost-members-csv-5.38.0.tgz +0 -0
- package/components/{tryghost-members-events-service-5.36.1.tgz → tryghost-members-events-service-5.38.0.tgz} +0 -0
- package/components/tryghost-members-importer-5.38.0.tgz +0 -0
- package/components/tryghost-members-offers-5.38.0.tgz +0 -0
- package/components/tryghost-members-payments-5.38.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.38.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.38.0.tgz +0 -0
- package/components/tryghost-milestones-5.38.0.tgz +0 -0
- package/components/tryghost-minifier-5.38.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.38.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.38.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.38.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.38.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.38.0.tgz +0 -0
- package/components/tryghost-mw-version-match-5.38.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.38.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.38.0.tgz +0 -0
- package/components/tryghost-package-json-5.38.0.tgz +0 -0
- package/components/tryghost-referrers-5.38.0.tgz +0 -0
- package/components/tryghost-security-5.38.0.tgz +0 -0
- package/components/{tryghost-session-service-5.36.1.tgz → tryghost-session-service-5.38.0.tgz} +0 -0
- package/components/tryghost-settings-path-manager-5.38.0.tgz +0 -0
- package/components/tryghost-slack-notifications-5.38.0.tgz +0 -0
- package/components/tryghost-staff-service-5.38.0.tgz +0 -0
- package/components/tryghost-stats-service-5.38.0.tgz +0 -0
- package/components/tryghost-tiers-5.38.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.38.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.38.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.38.0.tgz +0 -0
- package/components/tryghost-webmentions-5.38.0.tgz +0 -0
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +44 -30
- package/content/themes/casper/default.hbs +2 -2
- package/content/themes/casper/error.hbs +2 -2
- package/content/themes/casper/package.json +1 -1
- package/core/boot.js +15 -6
- package/core/built/admin/assets/chunk.143.c6802c882a911797ce4f.js +49 -0
- package/core/built/admin/assets/chunk.178.09faefd4027fcba4113d.js +10 -0
- package/core/built/admin/assets/{chunk.502.800e1515996bcc900013.js → chunk.220.9ca2950240aba3fced21.js} +2168 -2035
- package/core/built/admin/assets/{chunk.79.53e8aa9671b2d5dae8ba.js → chunk.79.acb7dd01e1c785f4920c.js} +191 -183
- package/core/built/admin/assets/{ghost-b828e9e3c161aae92909c2e163656bb1.js → ghost-35103ff053c43f1dfa7f35821c3c2412.js} +271 -249
- package/core/built/admin/assets/ghost-a9307c9cfe26a4bc621e02cd3bae421a.css +1 -0
- package/core/built/admin/assets/ghost-dark-f309cf445255344e4861a95ecb8f1920.css +1 -0
- package/core/built/admin/assets/{vendor-c4684647d4f5213e5dbb6763de430e7e.js → vendor-b982e3bf1020bff77b2a3c44d5f59e55.js} +442 -457
- package/core/built/admin/index.html +6 -6
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +1 -5
- package/core/frontend/helpers/ghost_head.js +4 -1
- package/core/frontend/meta/asset-url.js +9 -0
- package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
- package/core/frontend/services/sitemap/base-generator.js +5 -1
- package/core/server/adapters/storage/LocalImagesStorage.js +1 -1
- package/core/server/api/endpoints/db.js +17 -0
- package/core/server/api/endpoints/email-previews.js +2 -43
- package/core/server/api/endpoints/emails.js +1 -22
- package/core/server/api/endpoints/mentions.js +2 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/emails.js +14 -8
- package/core/server/api/endpoints/utils/serializers/output/posts.js +2 -2
- package/core/server/data/db/backup.js +13 -13
- package/core/server/data/importer/handlers/image.js +2 -2
- package/core/server/data/importer/import-manager.js +64 -5
- package/core/server/data/importer/importers/ContentFileImporter.js +128 -0
- package/core/server/data/migrations/versions/4.9/05-fix-missed-mobiledoc-url-transforms.js +1 -1
- package/core/server/data/schema/commands.js +21 -10
- package/core/server/lib/common/events.js +16 -23
- package/core/server/models/base/plugins/relations.js +5 -3
- package/core/server/models/index.js +5 -0
- package/core/server/models/mention.js +13 -0
- package/core/server/run-update-check.js +3 -1
- package/core/server/services/comments/emails.js +2 -2
- package/core/server/services/email-service/wrapper.js +2 -0
- package/core/server/services/link-tracking/LinkClickRepository.js +1 -1
- package/core/server/services/media-inliner/index.js +1 -0
- package/core/server/services/media-inliner/service.js +62 -0
- package/core/server/services/members/stats/members-stats.js +13 -9
- package/core/server/services/mentions/BookshelfMentionRepository.js +12 -1
- package/core/server/services/mentions/MentionController.js +7 -1
- package/core/server/services/mentions/WebmentionMetadata.js +3 -2
- package/core/server/services/mentions/service.js +5 -3
- package/core/server/services/posts/posts-service.js +3 -14
- package/core/server/{analytics-events.js → services/segment/index.js} +4 -3
- package/core/server/services/staff/index.js +2 -0
- package/core/server/services/stripe/config.js +4 -0
- package/core/server/services/url/Urls.js +10 -2
- package/core/server/update-check.js +5 -3
- package/core/server/web/api/endpoints/admin/app.js +5 -4
- package/core/server/web/api/endpoints/admin/routes.js +6 -0
- package/core/server/web/api/middleware/index.js +1 -2
- package/core/shared/config/overrides.json +34 -0
- package/core/shared/labs.js +3 -3
- package/package.json +164 -159
- package/yarn.lock +887 -934
- package/components/tryghost-adapter-cache-memory-ttl-5.36.1.tgz +0 -0
- package/components/tryghost-adapter-cache-redis-5.36.1.tgz +0 -0
- package/components/tryghost-adapter-manager-5.36.1.tgz +0 -0
- package/components/tryghost-api-framework-5.36.1.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.36.1.tgz +0 -0
- package/components/tryghost-audience-feedback-5.36.1.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.36.1.tgz +0 -0
- package/components/tryghost-constants-5.36.1.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.36.1.tgz +0 -0
- package/components/tryghost-data-generator-5.36.1.tgz +0 -0
- package/components/tryghost-domain-events-5.36.1.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.36.1.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.36.1.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.36.1.tgz +0 -0
- package/components/tryghost-email-events-5.36.1.tgz +0 -0
- package/components/tryghost-email-service-5.36.1.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.36.1.tgz +0 -0
- package/components/tryghost-event-aware-cache-wrapper-5.36.1.tgz +0 -0
- package/components/tryghost-extract-api-key-5.36.1.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.36.1.tgz +0 -0
- package/components/tryghost-i18n-5.36.1.tgz +0 -0
- package/components/tryghost-importer-revue-5.36.1.tgz +0 -0
- package/components/tryghost-job-manager-5.36.1.tgz +0 -0
- package/components/tryghost-link-redirects-5.36.1.tgz +0 -0
- package/components/tryghost-link-replacer-5.36.1.tgz +0 -0
- package/components/tryghost-link-tracking-5.36.1.tgz +0 -0
- package/components/tryghost-magic-link-5.36.1.tgz +0 -0
- package/components/tryghost-mailgun-client-5.36.1.tgz +0 -0
- package/components/tryghost-member-attribution-5.36.1.tgz +0 -0
- package/components/tryghost-member-events-5.36.1.tgz +0 -0
- package/components/tryghost-members-api-5.36.1.tgz +0 -0
- package/components/tryghost-members-csv-5.36.1.tgz +0 -0
- package/components/tryghost-members-importer-5.36.1.tgz +0 -0
- package/components/tryghost-members-offers-5.36.1.tgz +0 -0
- package/components/tryghost-members-payments-5.36.1.tgz +0 -0
- package/components/tryghost-members-ssr-5.36.1.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.36.1.tgz +0 -0
- package/components/tryghost-milestones-5.36.1.tgz +0 -0
- package/components/tryghost-minifier-5.36.1.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.36.1.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.36.1.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.36.1.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.36.1.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.36.1.tgz +0 -0
- package/components/tryghost-mw-vhost-5.36.1.tgz +0 -0
- package/components/tryghost-oembed-service-5.36.1.tgz +0 -0
- package/components/tryghost-package-json-5.36.1.tgz +0 -0
- package/components/tryghost-referrers-5.36.1.tgz +0 -0
- package/components/tryghost-security-5.36.1.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.36.1.tgz +0 -0
- package/components/tryghost-slack-notifications-5.36.1.tgz +0 -0
- package/components/tryghost-staff-service-5.36.1.tgz +0 -0
- package/components/tryghost-stats-service-5.36.1.tgz +0 -0
- package/components/tryghost-tiers-5.36.1.tgz +0 -0
- package/components/tryghost-update-check-service-5.36.1.tgz +0 -0
- package/components/tryghost-verification-trigger-5.36.1.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.36.1.tgz +0 -0
- package/components/tryghost-webmentions-5.36.1.tgz +0 -0
- package/core/built/admin/assets/chunk.143.26ea9f26571d656653f0.js +0 -49
- package/core/built/admin/assets/chunk.178.dd71b3a764b73facc400.js +0 -11
- package/core/built/admin/assets/ghost-7ecf5c7934d90798485ee5ac2956f7fe.css +0 -1
- package/core/built/admin/assets/ghost-dark-e50717df8e57d3e7fee67a0bcea895ad.css +0 -1
- package/core/frontend/src/cards/css/before-after.css +0 -81
- package/core/frontend/src/cards/js/before-after.js +0 -36
- package/core/server/data/importer/importers/image.js +0 -76
- package/core/server/data/schema/clients/index.js +0 -7
- package/core/server/data/schema/clients/mysql.js +0 -34
- package/core/server/data/schema/clients/sqlite3.js +0 -39
- package/core/server/services/bulk-email/bulk-email-processor.js +0 -289
- package/core/server/services/bulk-email/index.js +0 -1
- package/core/server/services/mega/email-preview.js +0 -54
- package/core/server/services/mega/feedback-buttons.js +0 -66
- package/core/server/services/mega/index.js +0 -14
- package/core/server/services/mega/mega.js +0 -626
- package/core/server/services/mega/post-email-serializer.js +0 -559
- package/core/server/services/mega/segment-parser.js +0 -20
- package/core/server/services/mega/template.js +0 -1319
- package/core/server/services/mentions/WebmentionRequest.js +0 -20
- package/core/server/web/admin/middleware.js +0 -17
- package/core/server/web/api/middleware/version-match.js +0 -31
- /package/core/built/admin/assets/{chunk.502.800e1515996bcc900013.js.LICENSE.txt → chunk.220.9ca2950240aba3fced21.js.LICENSE.txt} +0 -0
|
@@ -6,7 +6,6 @@ const tpl = require('@tryghost/tpl');
|
|
|
6
6
|
const db = require('../db');
|
|
7
7
|
const DatabaseInfo = require('@tryghost/database-info');
|
|
8
8
|
const schema = require('./schema');
|
|
9
|
-
const clients = require('./clients');
|
|
10
9
|
|
|
11
10
|
const messages = {
|
|
12
11
|
hasPrimaryKeySQLiteError: 'Must use hasPrimaryKeySQLite on an SQLite3 database',
|
|
@@ -432,11 +431,15 @@ function deleteTable(table, transaction = db.knex) {
|
|
|
432
431
|
/**
|
|
433
432
|
* @param {import('knex').Knex} [transaction] - connection to the DB
|
|
434
433
|
*/
|
|
435
|
-
function getTables(transaction = db.knex) {
|
|
434
|
+
async function getTables(transaction = db.knex) {
|
|
436
435
|
const client = transaction.client.config.client;
|
|
437
436
|
|
|
438
|
-
if (
|
|
439
|
-
|
|
437
|
+
if (client === 'sqlite3') {
|
|
438
|
+
const response = await transaction.raw('select * from sqlite_master where type = "table"');
|
|
439
|
+
return _.reject(_.map(response, 'tbl_name'), name => name === 'sqlite_sequence');
|
|
440
|
+
} else if (client === 'mysql2') {
|
|
441
|
+
const response = await transaction.raw('show tables');
|
|
442
|
+
return _.flatten(_.map(response[0], entry => _.values(entry)));
|
|
440
443
|
}
|
|
441
444
|
|
|
442
445
|
return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
|
|
@@ -446,11 +449,15 @@ function getTables(transaction = db.knex) {
|
|
|
446
449
|
* @param {string} table
|
|
447
450
|
* @param {import('knex').Knex} [transaction] - connection to the DB
|
|
448
451
|
*/
|
|
449
|
-
function getIndexes(table, transaction = db.knex) {
|
|
452
|
+
async function getIndexes(table, transaction = db.knex) {
|
|
450
453
|
const client = transaction.client.config.client;
|
|
451
454
|
|
|
452
|
-
if (
|
|
453
|
-
|
|
455
|
+
if (client === 'sqlite3') {
|
|
456
|
+
const response = await transaction.raw(`pragma index_list("${table}")`);
|
|
457
|
+
return _.flatten(_.map(response, 'name'));
|
|
458
|
+
} else if (client === 'mysql2') {
|
|
459
|
+
const response = await transaction.raw(`SHOW INDEXES from ${table}`);
|
|
460
|
+
return _.flatten(_.map(response[0], 'Key_name'));
|
|
454
461
|
}
|
|
455
462
|
|
|
456
463
|
return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
|
|
@@ -460,11 +467,15 @@ function getIndexes(table, transaction = db.knex) {
|
|
|
460
467
|
* @param {string} table
|
|
461
468
|
* @param {import('knex').Knex} [transaction] - connection to the DB
|
|
462
469
|
*/
|
|
463
|
-
function getColumns(table, transaction = db.knex) {
|
|
470
|
+
async function getColumns(table, transaction = db.knex) {
|
|
464
471
|
const client = transaction.client.config.client;
|
|
465
472
|
|
|
466
|
-
if (
|
|
467
|
-
|
|
473
|
+
if (client === 'sqlite3') {
|
|
474
|
+
const response = await transaction.raw(`pragma table_info("${table}")`);
|
|
475
|
+
return _.flatten(_.map(response, 'name'));
|
|
476
|
+
} else if (client === 'mysql2') {
|
|
477
|
+
const response = await transaction.raw(`SHOW COLUMNS from ${table}`);
|
|
478
|
+
return _.flatten(_.map(response[0], 'Field'));
|
|
468
479
|
}
|
|
469
480
|
|
|
470
481
|
return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
|
|
@@ -12,29 +12,22 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const events = require('events');
|
|
15
|
-
const util = require('util');
|
|
16
|
-
let EventRegistry;
|
|
17
|
-
let EventRegistryInstance;
|
|
18
15
|
|
|
19
|
-
EventRegistry
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
class EventRegistry extends events.EventEmitter {
|
|
17
|
+
/**
|
|
18
|
+
* This is method is semi-hack to make sure listeners are only registered once
|
|
19
|
+
* during the lifetime of the process. And example problem it solves is
|
|
20
|
+
* registering duplicate listeners between Ghost instance reboots when running tests.
|
|
21
|
+
* @param {String} eventName
|
|
22
|
+
* @param {String} listenerName named function name registered as a listener for the event
|
|
23
|
+
* @returns {Boolean}
|
|
24
|
+
*/
|
|
25
|
+
hasRegisteredListener(eventName, listenerName) {
|
|
26
|
+
return !!(this.listeners(eventName).find(listener => (listener.name === listenerName)));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
const eventRegistryInstance = new EventRegistry();
|
|
31
|
+
eventRegistryInstance.setMaxListeners(100);
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
* This is method is semi-hack to make sure listeners are only registered once
|
|
27
|
-
* during the lifetime of the process. And example problem it solves is
|
|
28
|
-
* registering duplicate listeners between Ghost instance reboots when running tests.
|
|
29
|
-
* @param {String} eventName
|
|
30
|
-
* @param {String} listenerName named function name registered as a listener for the event
|
|
31
|
-
* @returns {Boolean}
|
|
32
|
-
*/
|
|
33
|
-
EventRegistry.prototype.hasRegisteredListener = function (eventName, listenerName) {
|
|
34
|
-
return !!(this.listeners(eventName).find(listener => (listener.name === listenerName)));
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
EventRegistryInstance = new EventRegistry();
|
|
38
|
-
EventRegistryInstance.setMaxListeners(100);
|
|
39
|
-
|
|
40
|
-
module.exports = EventRegistryInstance;
|
|
33
|
+
module.exports = eventRegistryInstance;
|
|
@@ -7,7 +7,7 @@ module.exports = function (Bookshelf) {
|
|
|
7
7
|
* Return a relation, and load it if it hasn't been loaded already (or force a refresh with the forceRefresh option).
|
|
8
8
|
* refs https://github.com/TryGhost/Team/issues/1626
|
|
9
9
|
* @param {string} name Name of the relation to load
|
|
10
|
-
* @param {Object} [options] Options to pass to the fetch when not yet loaded (or when force refreshing)
|
|
10
|
+
* @param {Object} [options] Options to pass to the fetch when not yet loaded (or when force refreshing)
|
|
11
11
|
* @param {boolean} [options.forceRefresh] If true, the relation will be fetched again even if it has already been loaded.
|
|
12
12
|
* @returns {Promise<import('bookshelf').Model|import('bookshelf').Collection|null>}
|
|
13
13
|
*/
|
|
@@ -23,8 +23,10 @@ module.exports = function (Bookshelf) {
|
|
|
23
23
|
// Not yet loaded, or force refresh
|
|
24
24
|
// Note that we don't use .refresh on the relation on options.forceRefresh
|
|
25
25
|
// Because the relation can also be a collection, which doesn't have a refresh method
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const instance = this[name]();
|
|
27
|
+
await instance.fetch(options);
|
|
28
|
+
this.relations[name] = instance;
|
|
29
|
+
return instance;
|
|
28
30
|
}
|
|
29
31
|
});
|
|
30
32
|
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const _ = require('lodash');
|
|
6
|
+
const debug = require('@tryghost/debug')('models');
|
|
6
7
|
const glob = require('glob');
|
|
7
8
|
|
|
8
9
|
// enable event listeners
|
|
@@ -14,12 +15,16 @@ require('./base/listeners');
|
|
|
14
15
|
exports = module.exports;
|
|
15
16
|
|
|
16
17
|
function init() {
|
|
18
|
+
const baseNow = Date.now();
|
|
17
19
|
exports.Base = require('./base');
|
|
20
|
+
debug(`${Date.now() - baseNow}ms - Base.js require`);
|
|
18
21
|
|
|
19
22
|
let modelsFiles = glob.sync('!(index).js', {cwd: __dirname});
|
|
20
23
|
modelsFiles.forEach((model) => {
|
|
21
24
|
const name = model.replace(/.js$/, '');
|
|
25
|
+
const modelNow = Date.now();
|
|
22
26
|
_.extend(exports, require('./' + name));
|
|
27
|
+
debug(`${Date.now() - modelNow}ms - ${model} require`);
|
|
23
28
|
});
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -9,6 +9,19 @@ const Mention = ghostBookshelf.Model.extend({
|
|
|
9
9
|
enforcedFilters() {
|
|
10
10
|
return 'deleted:false';
|
|
11
11
|
}
|
|
12
|
+
}, {
|
|
13
|
+
permittedOptions(methodName) {
|
|
14
|
+
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
|
15
|
+
const validOptions = {
|
|
16
|
+
findPage: ['selectRaw', 'whereRaw']
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
if (validOptions[methodName]) {
|
|
20
|
+
options = options.concat(validOptions[methodName]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return options;
|
|
24
|
+
}
|
|
12
25
|
});
|
|
13
26
|
|
|
14
27
|
module.exports = {
|
|
@@ -2,7 +2,7 @@ const {promises: fs} = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const moment = require('moment');
|
|
4
4
|
const htmlToPlaintext = require('@tryghost/html-to-plaintext');
|
|
5
|
-
const
|
|
5
|
+
const emailService = require('../email-service');
|
|
6
6
|
|
|
7
7
|
class CommentsServiceEmails {
|
|
8
8
|
constructor({config, logging, models, mailer, settingsCache, settingsHelpers, urlService, urlUtils}) {
|
|
@@ -95,7 +95,7 @@ class CommentsServiceEmails {
|
|
|
95
95
|
accentColor: this.settingsCache.get('accent_color'),
|
|
96
96
|
fromEmail: this.notificationFromAddress,
|
|
97
97
|
toEmail: to,
|
|
98
|
-
profileUrl:
|
|
98
|
+
profileUrl: emailService.renderer.createUnsubscribeUrl(member.get('uuid'), {comments: true})
|
|
99
99
|
};
|
|
100
100
|
|
|
101
101
|
const {html, text} = await this.renderEmailTemplate('new-comment-reply', templateData);
|
|
@@ -56,7 +56,7 @@ module.exports = class LinkClickRepository {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const model = await this.#MemberLinkClickEventModel.add({
|
|
59
|
-
// Only store the
|
|
59
|
+
// Only store the pathname (no support for variable query strings)
|
|
60
60
|
redirect_id: linkClick.link_id.toHexString(),
|
|
61
61
|
member_id: member.id
|
|
62
62
|
}, {});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./service');
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
async init() {
|
|
3
|
+
const debug = require('@tryghost/debug')('mediaInliner');
|
|
4
|
+
const MediaInliner = require('@tryghost/external-media-inliner');
|
|
5
|
+
const models = require('../../models');
|
|
6
|
+
const jobsService = require('../jobs');
|
|
7
|
+
|
|
8
|
+
const mediaStorage = require('../../adapters/storage').getStorage('media');
|
|
9
|
+
const imageStorage = require('../../adapters/storage').getStorage('images');
|
|
10
|
+
const fileStorage = require('../../adapters/storage').getStorage('files');
|
|
11
|
+
|
|
12
|
+
const config = require('../../../shared/config');
|
|
13
|
+
|
|
14
|
+
const mediaInliner = new MediaInliner({
|
|
15
|
+
PostModel: models.Post,
|
|
16
|
+
TagModel: models.Tag,
|
|
17
|
+
UserModel: models.User,
|
|
18
|
+
PostMetaModel: models.PostsMeta,
|
|
19
|
+
getMediaStorage: (extension) => {
|
|
20
|
+
if (config.get('uploads').images.extensions.includes(extension)) {
|
|
21
|
+
return imageStorage;
|
|
22
|
+
} else if (config.get('uploads').media.extensions.includes(extension)) {
|
|
23
|
+
return mediaStorage;
|
|
24
|
+
} else if (config.get('uploads').files.extensions.includes(extension)) {
|
|
25
|
+
return fileStorage;
|
|
26
|
+
} else {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.api = {
|
|
33
|
+
|
|
34
|
+
startMediaInliner: async (domains) => {
|
|
35
|
+
if (!domains || !domains.length) {
|
|
36
|
+
// default domains to inline from if none are provided
|
|
37
|
+
domains = [
|
|
38
|
+
'https://s3.amazonaws.com/revue',
|
|
39
|
+
'https://substackcdn.com'
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
debug('[Inliner] Starting media inlining job for domains: ', domains);
|
|
44
|
+
|
|
45
|
+
// @NOTE: the job is "inline" (aka non-offloaded into a thread), because usecases are currently
|
|
46
|
+
// limited to migrational, so there is no expectations for site's availability etc.
|
|
47
|
+
await jobsService.addJob({
|
|
48
|
+
name: 'external-media-inliner',
|
|
49
|
+
job: (data) => {
|
|
50
|
+
return mediaInliner.inline(data.domains);
|
|
51
|
+
},
|
|
52
|
+
data: {domains},
|
|
53
|
+
offloaded: false
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
status: 'success'
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const moment = require('moment-timezone');
|
|
2
|
-
const Promise = require('bluebird');
|
|
3
2
|
|
|
4
3
|
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
|
5
4
|
class MembersStats {
|
|
@@ -131,14 +130,19 @@ class MembersStats {
|
|
|
131
130
|
const totalMembers = await this.getTotalMembers();
|
|
132
131
|
|
|
133
132
|
// perform final calculations in parallel
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return
|
|
133
|
+
const [total, totalInRange, totalOnDate, newToday] = await Promise.all([
|
|
134
|
+
totalMembers,
|
|
135
|
+
this.getTotalMembersInRange({days, totalMembers, siteTimezone}),
|
|
136
|
+
this.getTotalMembersOnDatesInRange({days, totalMembers, siteTimezone}),
|
|
137
|
+
this.getNewMembersToday({siteTimezone})
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
total,
|
|
142
|
+
total_in_range: totalInRange,
|
|
143
|
+
total_on_date: totalOnDate,
|
|
144
|
+
new_today: newToday
|
|
145
|
+
};
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -65,7 +65,18 @@ module.exports = class BookshelfMentionRepository {
|
|
|
65
65
|
* @returns {Promise<Page<import('@tryghost/webmentions/lib/Mention')>>}
|
|
66
66
|
*/
|
|
67
67
|
async getPage(options) {
|
|
68
|
-
|
|
68
|
+
/**
|
|
69
|
+
* @type {GetPageOptions & {whereRaw?: string}}
|
|
70
|
+
*/
|
|
71
|
+
const _options = {
|
|
72
|
+
...options
|
|
73
|
+
};
|
|
74
|
+
delete _options.unique;
|
|
75
|
+
if (options.unique) {
|
|
76
|
+
_options.whereRaw = 'NOT EXISTS (select id from mentions as m where m.id > mentions.id and m.source = mentions.source)';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const page = await this.#MentionModel.findPage(_options);
|
|
69
80
|
|
|
70
81
|
return {
|
|
71
82
|
data: await Promise.all(page.data.map(model => this.#modelToMention(model))),
|
|
@@ -80,11 +80,17 @@ module.exports = class MentionController {
|
|
|
80
80
|
order = 'created_at asc';
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
let unique;
|
|
84
|
+
if (frame.options.unique && (frame.options.unique === 'true' || frame.options.unique === true)) {
|
|
85
|
+
unique = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
83
88
|
const mentions = await this.#api.listMentions({
|
|
84
89
|
filter: frame.options.filter,
|
|
85
90
|
order,
|
|
86
91
|
limit,
|
|
87
|
-
page
|
|
92
|
+
page,
|
|
93
|
+
unique
|
|
88
94
|
});
|
|
89
95
|
|
|
90
96
|
const resources = await Promise.all(mentions.data.map((mention) => {
|
|
@@ -6,14 +6,15 @@ module.exports = class WebmentionMetadata {
|
|
|
6
6
|
* @returns {Promise<import('@tryghost/webmentions/lib/MentionsAPI').WebmentionMetadata>}
|
|
7
7
|
*/
|
|
8
8
|
async fetch(url) {
|
|
9
|
-
const data = await oembedService.fetchOembedDataFromUrl(url.href, '
|
|
9
|
+
const data = await oembedService.fetchOembedDataFromUrl(url.href, 'mention');
|
|
10
10
|
const result = {
|
|
11
11
|
siteTitle: data.metadata.publisher,
|
|
12
12
|
title: data.metadata.title,
|
|
13
13
|
excerpt: data.metadata.description,
|
|
14
14
|
author: data.metadata.author,
|
|
15
15
|
image: data.metadata.thumbnail ? new URL(data.metadata.thumbnail) : null,
|
|
16
|
-
favicon: data.metadata.icon ? new URL(data.metadata.icon) : null
|
|
16
|
+
favicon: data.metadata.icon ? new URL(data.metadata.icon) : null,
|
|
17
|
+
body: data.body
|
|
17
18
|
};
|
|
18
19
|
return result;
|
|
19
20
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const MentionController = require('./MentionController');
|
|
2
2
|
const WebmentionMetadata = require('./WebmentionMetadata');
|
|
3
|
-
const WebmentionRequest = require('./WebmentionRequest');
|
|
4
3
|
const {
|
|
5
4
|
MentionsAPI,
|
|
6
5
|
MentionSendingService,
|
|
@@ -27,13 +26,17 @@ function getPostUrl(post) {
|
|
|
27
26
|
|
|
28
27
|
module.exports = {
|
|
29
28
|
controller: new MentionController(),
|
|
29
|
+
didInit: false,
|
|
30
30
|
async init() {
|
|
31
|
+
if (this.didInit) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.didInit = true;
|
|
31
35
|
const repository = new BookshelfMentionRepository({
|
|
32
36
|
MentionModel: models.Mention,
|
|
33
37
|
DomainEvents
|
|
34
38
|
});
|
|
35
39
|
const webmentionMetadata = new WebmentionMetadata();
|
|
36
|
-
const webmentionRequest = new WebmentionRequest();
|
|
37
40
|
const discoveryService = new MentionDiscoveryService({externalRequest});
|
|
38
41
|
const resourceService = new ResourceService({
|
|
39
42
|
urlUtils,
|
|
@@ -49,7 +52,6 @@ module.exports = {
|
|
|
49
52
|
const api = new MentionsAPI({
|
|
50
53
|
repository,
|
|
51
54
|
webmentionMetadata,
|
|
52
|
-
webmentionRequest,
|
|
53
55
|
resourceService,
|
|
54
56
|
routingService
|
|
55
57
|
});
|
|
@@ -8,8 +8,7 @@ const messages = {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
class PostsService {
|
|
11
|
-
constructor({
|
|
12
|
-
this.mega = mega;
|
|
11
|
+
constructor({urlUtils, models, isSet, stats, emailService}) {
|
|
13
12
|
this.urlUtils = urlUtils;
|
|
14
13
|
this.models = models;
|
|
15
14
|
this.isSet = isSet;
|
|
@@ -45,17 +44,9 @@ class PostsService {
|
|
|
45
44
|
let email;
|
|
46
45
|
|
|
47
46
|
if (!postEmail) {
|
|
48
|
-
|
|
49
|
-
email = await this.emailService.createEmail(model);
|
|
50
|
-
} else {
|
|
51
|
-
email = await this.mega.addEmail(model, frame.options);
|
|
52
|
-
}
|
|
47
|
+
email = await this.emailService.createEmail(model);
|
|
53
48
|
} else if (postEmail && postEmail.get('status') === 'failed') {
|
|
54
|
-
|
|
55
|
-
email = await this.emailService.retryEmail(postEmail);
|
|
56
|
-
} else {
|
|
57
|
-
email = await this.mega.retryFailedEmail(postEmail);
|
|
58
|
-
}
|
|
49
|
+
email = await this.emailService.retryEmail(postEmail);
|
|
59
50
|
}
|
|
60
51
|
if (email) {
|
|
61
52
|
model.set('email', email);
|
|
@@ -130,7 +121,6 @@ class PostsService {
|
|
|
130
121
|
*/
|
|
131
122
|
const getPostServiceInstance = () => {
|
|
132
123
|
const urlUtils = require('../../../shared/url-utils');
|
|
133
|
-
const {mega} = require('../mega');
|
|
134
124
|
const labs = require('../../../shared/labs');
|
|
135
125
|
const models = require('../../models');
|
|
136
126
|
const PostStats = require('./stats/post-stats');
|
|
@@ -139,7 +129,6 @@ const getPostServiceInstance = () => {
|
|
|
139
129
|
const postStats = new PostStats();
|
|
140
130
|
|
|
141
131
|
return new PostsService({
|
|
142
|
-
mega: mega,
|
|
143
132
|
urlUtils: urlUtils,
|
|
144
133
|
models: models,
|
|
145
134
|
isSet: flag => labs.isSet(flag), // don't use bind, that breaks test subbing of labs
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const Analytics = require('analytics-node');
|
|
3
|
-
const config = require('../shared/config');
|
|
4
3
|
const logging = require('@tryghost/logging');
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
const config = require('../../../shared/config');
|
|
6
|
+
const sentry = require('../../../shared/sentry');
|
|
6
7
|
|
|
7
8
|
// Listens to model events to layer on analytics - also uses the "fake" theme.uploaded event from the theme API
|
|
8
|
-
const events = require('
|
|
9
|
+
const events = require('../../lib/common/events');
|
|
9
10
|
|
|
10
11
|
module.exports.init = function () {
|
|
11
12
|
const analytics = new Analytics(config.get('segment:key'));
|
|
@@ -12,6 +12,7 @@ class StaffServiceWrapper {
|
|
|
12
12
|
|
|
13
13
|
const logging = require('@tryghost/logging');
|
|
14
14
|
const models = require('../../models');
|
|
15
|
+
const memberAttribution = require('../member-attribution');
|
|
15
16
|
const {GhostMailer} = require('../mail');
|
|
16
17
|
const mailer = new GhostMailer();
|
|
17
18
|
const settingsCache = require('../../../shared/settings-cache');
|
|
@@ -26,6 +27,7 @@ class StaffServiceWrapper {
|
|
|
26
27
|
settingsCache,
|
|
27
28
|
urlUtils,
|
|
28
29
|
DomainEvents,
|
|
30
|
+
memberAttributionService: memberAttribution.service,
|
|
29
31
|
labs
|
|
30
32
|
});
|
|
31
33
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
2
|
const tpl = require('@tryghost/tpl');
|
|
3
|
+
const labs = require('../../../shared/labs');
|
|
3
4
|
|
|
4
5
|
const messages = {
|
|
5
6
|
remoteWebhooksInDevelopment: 'Cannot use remote webhooks in development. See https://ghost.org/docs/webhooks/#stripe-webhooks for developing with Stripe.'
|
|
@@ -64,6 +65,9 @@ module.exports = {
|
|
|
64
65
|
...keys,
|
|
65
66
|
...urls,
|
|
66
67
|
enablePromoCodes: config.get('enableStripePromoCodes'),
|
|
68
|
+
get enableAutomaticTax() {
|
|
69
|
+
return labs.isSet('stripeAutomaticTax');
|
|
70
|
+
},
|
|
67
71
|
webhookSecret: webhookSecret,
|
|
68
72
|
webhookHandlerUrl: webhookHandlerUrl.href
|
|
69
73
|
};
|
|
@@ -44,10 +44,18 @@ class Urls {
|
|
|
44
44
|
debug('cache', url);
|
|
45
45
|
|
|
46
46
|
if (this.urls[resource.data.id]) {
|
|
47
|
-
|
|
47
|
+
const error = new errors.InternalServerError({
|
|
48
48
|
message: 'This should not happen.',
|
|
49
49
|
code: 'URLSERVICE_RESOURCE_DUPLICATE'
|
|
50
|
-
})
|
|
50
|
+
});
|
|
51
|
+
if (process.env.NODE_ENV.startsWith('test')) {
|
|
52
|
+
logging.warn({
|
|
53
|
+
message: 'Duplicate URL',
|
|
54
|
+
err: error
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
logging.error(error);
|
|
58
|
+
}
|
|
51
59
|
|
|
52
60
|
this.removeResourceId(resource.data.id);
|
|
53
61
|
}
|
|
@@ -12,10 +12,11 @@ const UpdateCheckService = require('@tryghost/update-check-service');
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Initializes and triggers update check
|
|
15
|
-
*
|
|
15
|
+
* @param {Object} [options]
|
|
16
|
+
* @param {Boolean} [options.rethrowErrors] - if true, errors will be thrown instead of logged
|
|
16
17
|
* @returns {Promise<any>}
|
|
17
18
|
*/
|
|
18
|
-
module.exports = async () => {
|
|
19
|
+
module.exports = async ({rethrowErrors = false} = {}) => {
|
|
19
20
|
const allowedCheckEnvironments = ['development', 'production'];
|
|
20
21
|
|
|
21
22
|
// CASE: The check will not happen if your NODE_ENV is not in the allowed defined environments.
|
|
@@ -51,7 +52,8 @@ module.exports = async () => {
|
|
|
51
52
|
notificationGroups: config.get('notificationGroups'),
|
|
52
53
|
siteUrl: urlUtils.urlFor('home', true),
|
|
53
54
|
forceUpdate: config.get('updateCheck:forceUpdate'),
|
|
54
|
-
ghostVersion: ghostVersion.original
|
|
55
|
+
ghostVersion: ghostVersion.original,
|
|
56
|
+
rethrowErrors
|
|
55
57
|
},
|
|
56
58
|
request,
|
|
57
59
|
sendEmail: ghostMailer.send.bind(ghostMailer)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('web:endpoints:admin:app');
|
|
2
2
|
const boolParser = require('express-query-boolean');
|
|
3
|
-
const express = require('../../../../../shared/express');
|
|
4
3
|
const bodyParser = require('body-parser');
|
|
5
|
-
const shared = require('../../../shared');
|
|
6
|
-
const apiMw = require('../../middleware');
|
|
7
4
|
const errorHandler = require('@tryghost/mw-error-handler');
|
|
5
|
+
const versionMatch = require('@tryghost/mw-version-match');
|
|
6
|
+
|
|
7
|
+
const shared = require('../../../shared');
|
|
8
|
+
const express = require('../../../../../shared/express');
|
|
8
9
|
const sentry = require('../../../../../shared/sentry');
|
|
9
10
|
const routes = require('./routes');
|
|
10
11
|
const APIVersionCompatibilityService = require('../../../../services/api-version-compatibility');
|
|
@@ -24,7 +25,7 @@ module.exports = function setupApiApp() {
|
|
|
24
25
|
|
|
25
26
|
// Check version matches for API requests, depends on res.locals.safeVersion being set
|
|
26
27
|
// Therefore must come after themeHandler.ghostLocals, for now
|
|
27
|
-
apiApp.use(
|
|
28
|
+
apiApp.use(versionMatch);
|
|
28
29
|
|
|
29
30
|
// Admin API shouldn't be cached
|
|
30
31
|
apiApp.use(shared.middleware.cacheControl('private'));
|
|
@@ -205,6 +205,12 @@ module.exports = function apiRoutes() {
|
|
|
205
205
|
http(api.db.backupContent)
|
|
206
206
|
);
|
|
207
207
|
|
|
208
|
+
router.post('/db/media/inline',
|
|
209
|
+
mw.authAdminApi,
|
|
210
|
+
labs.enabledMiddleware('mediaInliner'),
|
|
211
|
+
http(api.db.inlineMedia)
|
|
212
|
+
);
|
|
213
|
+
|
|
208
214
|
// ## Slack
|
|
209
215
|
router.post('/slack/test', mw.authAdminApi, http(api.slack.sendTest));
|
|
210
216
|
|