ghost 4.20.3 → 4.22.1
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/.eslintrc.js +1 -1
- package/Gruntfile.js +1 -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 +263 -50
- package/content/themes/casper/default.hbs +12 -3
- package/content/themes/casper/index.hbs +25 -23
- package/content/themes/casper/package.json +91 -2
- package/content/themes/casper/partials/post-card.hbs +1 -1
- package/content/themes/casper/post.hbs +18 -14
- package/content/themes/casper/yarn.lock +245 -192
- package/core/boot.js +5 -0
- package/core/bridge.js +14 -0
- package/core/built/assets/{chunk.3.777d43e2ce954ba8b2f5.js → chunk.3.1148677ff3b78e5aeaee.js} +60 -60
- package/core/built/assets/{ghost-dark-20e2892d4f30d0d1183c9ac725ea37d0.css → ghost-dark-684ad238e1a858c7cb5be6988de7c6f5.css} +1 -1
- package/core/built/assets/{ghost.min-57e46fd3b1145ecf2cbd185a13611f3b.css → ghost.min-66e08535f8bb797a8c40e0a2b31f1e9e.css} +1 -1
- package/core/built/assets/{ghost.min-07b6a50c54b3e2e190332c28c7255d2f.js → ghost.min-f7037eca328f4d4eb99f0309c19c9bae.js} +1788 -1910
- package/core/built/assets/{vendor.min-af502ac4142871500fc424f6a5a254ec.js → vendor.min-7c8fdd90f7ecd2e94328a07ea3b64608.js} +985 -956
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +16 -4
- package/core/frontend/helpers/asset.js +9 -1
- package/core/frontend/helpers/ghost_head.js +13 -1
- package/core/frontend/meta/title.js +15 -5
- package/core/frontend/services/card-assets/index.js +16 -0
- package/core/frontend/services/card-assets/service.js +101 -0
- package/core/frontend/services/routing/router-manager.js +5 -3
- package/core/frontend/services/theme-engine/config/defaults.json +4 -1
- package/core/frontend/services/theme-engine/config/index.js +1 -1
- package/core/frontend/src/cards/css/bookmark.css +83 -0
- package/core/frontend/src/cards/css/gallery.css +36 -0
- package/core/frontend/src/cards/js/gallery.js +8 -0
- package/core/frontend/web/middleware/serve-public-file.js +10 -1
- package/core/frontend/web/site.js +10 -9
- package/core/server/adapters/storage/LocalImagesStorage.js +50 -0
- package/core/server/adapters/storage/LocalMediaStorage.js +23 -0
- package/core/server/adapters/storage/{LocalFileStorage.js → LocalStorageBase.js} +36 -48
- package/core/server/adapters/storage/index.js +1 -1
- package/core/server/adapters/storage/utils.js +2 -2
- package/core/server/api/canary/index.js +4 -0
- package/core/server/api/canary/media.js +22 -0
- package/core/server/api/canary/members.js +6 -103
- package/core/server/api/canary/membersStripeConnect.js +0 -10
- package/core/server/api/canary/redirects.js +1 -6
- package/core/server/api/canary/utils/serializers/input/pages.js +8 -0
- package/core/server/api/canary/utils/serializers/output/images.js +4 -0
- package/core/server/api/canary/utils/serializers/output/index.js +4 -0
- package/core/server/api/canary/utils/serializers/output/media.js +28 -0
- package/core/server/api/canary/utils/validators/input/index.js +4 -0
- package/core/server/api/canary/utils/validators/input/media.js +7 -0
- package/core/server/api/v2/redirects.js +1 -6
- package/core/server/api/v2/utils/serializers/output/images.js +4 -0
- package/core/server/api/v3/members.js +5 -1
- package/core/server/api/v3/redirects.js +1 -6
- package/core/server/api/v3/utils/serializers/output/images.js +4 -0
- package/core/server/data/migrations/utils.js +55 -16
- package/core/server/data/migrations/versions/4.22/01-add-is-launch-complete-setting.js +8 -0
- package/core/server/data/migrations/versions/4.22/02-update-launch-complete-setting-from-user-data.js +39 -0
- package/core/server/data/schema/default-settings.json +8 -0
- package/core/server/frontend/ghost.min.css +1 -1
- package/core/server/lib/image/blog-icon.js +2 -4
- package/core/server/lib/image/image-size.js +1 -1
- package/core/server/services/limits.js +3 -6
- package/core/server/services/mega/template.js +4 -0
- package/core/server/services/members/api.js +1 -1
- package/core/server/services/members/emails/signup.js +2 -2
- package/core/server/services/members/stripe-connect.js +14 -0
- package/core/server/services/offers/service.js +1 -31
- package/core/server/services/redirects/api.js +270 -0
- package/core/server/services/redirects/index.js +27 -12
- package/core/server/services/themes/ThemeStorage.js +5 -5
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/canary/admin/routes.js +13 -4
- package/core/server/web/api/middleware/upload.js +117 -10
- package/core/server/web/members/app.js +1 -1
- package/core/server/web/shared/middlewares/index.js +0 -4
- package/core/shared/config/defaults.json +3 -1
- package/core/shared/config/helpers.js +2 -0
- package/core/shared/config/overrides.json +8 -0
- package/core/shared/labs.js +5 -3
- package/package.json +35 -34
- package/yarn.lock +999 -1000
- package/core/built/assets/img/themes/Editorial-a25a4a34c04dedd858bd5e05ef388b1c.jpg +0 -0
- package/core/built/assets/img/themes/Massively-06edf00108429f7fb8e65f190fba34fe.jpg +0 -0
- package/core/server/services/redirects/settings.js +0 -234
- package/core/server/web/shared/middlewares/custom-redirects.js +0 -128
|
@@ -191,14 +191,8 @@ function addPermissionToRole(config) {
|
|
|
191
191
|
}).first();
|
|
192
192
|
|
|
193
193
|
if (!permission) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
action: 'remove',
|
|
197
|
-
permission: config.permission,
|
|
198
|
-
role: config.role,
|
|
199
|
-
resource: 'permission'
|
|
200
|
-
})
|
|
201
|
-
});
|
|
194
|
+
logging.warn(`Removing permission(${config.permission}) from role(${config.role}) - Permission not found.`);
|
|
195
|
+
return;
|
|
202
196
|
}
|
|
203
197
|
|
|
204
198
|
const role = await connection('roles').where({
|
|
@@ -206,14 +200,8 @@ function addPermissionToRole(config) {
|
|
|
206
200
|
}).first();
|
|
207
201
|
|
|
208
202
|
if (!role) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
action: 'remove',
|
|
212
|
-
permission: config.permission,
|
|
213
|
-
role: config.role,
|
|
214
|
-
resource: 'role'
|
|
215
|
-
})
|
|
216
|
-
});
|
|
203
|
+
logging.warn(`Removing permission(${config.permission}) from role(${config.role}) - Role not found.`);
|
|
204
|
+
return;
|
|
217
205
|
}
|
|
218
206
|
|
|
219
207
|
const existingRelation = await connection('permissions_roles').where({
|
|
@@ -421,12 +409,63 @@ function createDropColumnMigration(table, column, columnDefinition) {
|
|
|
421
409
|
);
|
|
422
410
|
}
|
|
423
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Creates a migration which will insert a new setting in settings table
|
|
414
|
+
* @param {object} settingSpec - setting key, value, group and type
|
|
415
|
+
*
|
|
416
|
+
* @returns {Object} migration object returning config/up/down properties
|
|
417
|
+
*/
|
|
418
|
+
function addSetting({key, value, type, group}) {
|
|
419
|
+
return createTransactionalMigration(
|
|
420
|
+
async function up(connection) {
|
|
421
|
+
const settingExists = await connection('settings')
|
|
422
|
+
.where('key', '=', key)
|
|
423
|
+
.first();
|
|
424
|
+
if (settingExists) {
|
|
425
|
+
logging.warn(`Skipping adding setting: ${key} - setting already exists`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
logging.info(`Adding setting: ${key}`);
|
|
430
|
+
const now = connection.raw('CURRENT_TIMESTAMP');
|
|
431
|
+
|
|
432
|
+
return connection('settings')
|
|
433
|
+
.insert({
|
|
434
|
+
id: ObjectId().toHexString(),
|
|
435
|
+
key,
|
|
436
|
+
value,
|
|
437
|
+
group,
|
|
438
|
+
type,
|
|
439
|
+
created_at: now,
|
|
440
|
+
created_by: MIGRATION_USER,
|
|
441
|
+
updated_at: now,
|
|
442
|
+
updated_by: MIGRATION_USER
|
|
443
|
+
});
|
|
444
|
+
},
|
|
445
|
+
async function down(connection) {
|
|
446
|
+
const settingExists = await connection('settings')
|
|
447
|
+
.where('key', '=', key)
|
|
448
|
+
.first();
|
|
449
|
+
if (!settingExists) {
|
|
450
|
+
logging.warn(`Skipping dropping setting: ${key} - setting does not exist`);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
logging.info(`Dropping setting: ${key}`);
|
|
455
|
+
return connection('settings')
|
|
456
|
+
.where('key', '=', key)
|
|
457
|
+
.del();
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
424
462
|
module.exports = {
|
|
425
463
|
addTable,
|
|
426
464
|
dropTables,
|
|
427
465
|
addPermission,
|
|
428
466
|
addPermissionToRole,
|
|
429
467
|
addPermissionWithRoles,
|
|
468
|
+
addSetting,
|
|
430
469
|
createTransactionalMigration,
|
|
431
470
|
createNonTransactionalMigration,
|
|
432
471
|
createIrreversibleMigration,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const {createTransactionalMigration} = require('../../utils.js');
|
|
3
|
+
|
|
4
|
+
module.exports = createTransactionalMigration(
|
|
5
|
+
async function up(knex) {
|
|
6
|
+
const userRows = await knex('users').select('accessibility').whereNotNull('accessibility');
|
|
7
|
+
const hasLaunchComplete = userRows.find((user) => {
|
|
8
|
+
try {
|
|
9
|
+
const userAccessibility = JSON.parse(user.accessibility);
|
|
10
|
+
return userAccessibility.launchComplete;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
if (hasLaunchComplete) {
|
|
16
|
+
logging.info('Updating setting: "editor_is_launch_complete" to "true"');
|
|
17
|
+
await knex('settings')
|
|
18
|
+
.where({
|
|
19
|
+
key: 'editor_is_launch_complete'
|
|
20
|
+
})
|
|
21
|
+
.update({
|
|
22
|
+
value: 'true'
|
|
23
|
+
});
|
|
24
|
+
} else {
|
|
25
|
+
logging.warn('Skipped setting update: "editor_is_launch_complete" setting - is already correct value!');
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
async function down(knex) {
|
|
30
|
+
logging.info('Reverting setting: "editor_is_launch_complete" - to "false"');
|
|
31
|
+
await knex('settings')
|
|
32
|
+
.where({
|
|
33
|
+
key: 'editor_is_launch_complete'
|
|
34
|
+
})
|
|
35
|
+
.update({
|
|
36
|
+
value: 'false'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
);
|
|
@@ -590,6 +590,14 @@
|
|
|
590
590
|
"editor_default_email_recipients_filter": {
|
|
591
591
|
"defaultValue": "all",
|
|
592
592
|
"type": "string"
|
|
593
|
+
},
|
|
594
|
+
"editor_is_launch_complete": {
|
|
595
|
+
"defaultValue": "false",
|
|
596
|
+
"validations": {
|
|
597
|
+
"isEmpty": false,
|
|
598
|
+
"isIn": [["true", "false"]]
|
|
599
|
+
},
|
|
600
|
+
"type": "boolean"
|
|
593
601
|
}
|
|
594
602
|
}
|
|
595
603
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;font-family:sans-serif}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.darkgrey{color:#343f44}.midgrey{color:#738a94}.lightgrey{color:#e5eff5}.blue{color:#3eb0ef}.red{color:#f05230}.orange{color:#fecd35}.green{color:#a4d037}.darkgrey-hover:hover{color:#343f44}.midgrey-hover:hover{color:#738a94}.lightgrey-hover:hover{color:#e5eff5}.blue-hover:hover{color:#3eb0ef}.red-hover:hover{color:#f05230}.orange-hover:hover{color:#fecd35}.green-hover:hover{color:#a4d037}*,:after,:before{box-sizing:border-box}html{-webkit-tap-highlight-color:rgba(0,0,0,0);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:62.5%;letter-spacing:.2px;line-height:1.65;overflow:hidden}body,html{height:100%;width:100%}body{color:#343f44;font-size:1.4rem;overflow:auto;overflow-x:hidden}.gh-view{-ms-flex-positive:1;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;flex-grow:1}h1,h2{text-rendering:optimizeLegibility;color:#343f44;font-size:2.9rem;line-height:1.15em;margin:0 0 .3em;text-indent:-1px}@media (max-width:500px){h1{font-size:2.4rem}}.gh-input{-webkit-appearance:none;border:1px solid #d6e3eb;border-radius:4px;color:#4b5b62;display:block;font-size:1.6rem;font-weight:300;height:40px;line-height:1em;padding:10px 12px;transition:border-color .15s linear;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;width:100%}.gh-input:focus{border-color:#b4cbda;outline:0}.gh-btn{fill:#829aa8;-webkit-font-smoothing:subpixel-antialiased;border:1px solid #d6e3eb;border-radius:5px;color:#829aa8;display:inline-block;outline:none;text-decoration:none!important;text-shadow:0 1px 0 #fff;transition:all .2s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.gh-btn span{border-radius:4px;display:block;font-size:1.3rem;font-weight:400;height:33px;letter-spacing:.2px;line-height:33px;padding:0 12px;text-align:center}.gh-btn:hover{border-color:#b4cbda}.gh-btn-hover-blue:hover{border-color:#3eb0ef;color:#3eb0ef}.gh-btn-blue{fill:#fff;background:linear-gradient(#3da1d6,#2288bf);border:0;box-shadow:0 1px 0 rgba(0,0,0,.12);color:#fff;padding:1px;text-shadow:0 -1px 0 rgba(0,0,0,.1);transition:none!important}.gh-btn-blue span{background:linear-gradient(#4ab6f0,#2fa5e4 60%,#2fa5e4 90%,#38a9e5);box-shadow:inset 0 1px 0
|
|
1
|
+
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;font-family:sans-serif}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.darkgrey{color:#343f44}.midgrey{color:#738a94}.lightgrey{color:#e5eff5}.blue{color:#3eb0ef}.red{color:#f05230}.orange{color:#fecd35}.green{color:#a4d037}.darkgrey-hover:hover{color:#343f44}.midgrey-hover:hover{color:#738a94}.lightgrey-hover:hover{color:#e5eff5}.blue-hover:hover{color:#3eb0ef}.red-hover:hover{color:#f05230}.orange-hover:hover{color:#fecd35}.green-hover:hover{color:#a4d037}*,:after,:before{box-sizing:border-box}html{-webkit-tap-highlight-color:rgba(0,0,0,0);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:62.5%;letter-spacing:.2px;line-height:1.65;overflow:hidden}body,html{height:100%;width:100%}body{color:#343f44;font-size:1.4rem;overflow:auto;overflow-x:hidden}.gh-view{-ms-flex-positive:1;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;flex-grow:1}h1,h2{text-rendering:optimizeLegibility;color:#343f44;font-size:2.9rem;line-height:1.15em;margin:0 0 .3em;text-indent:-1px}@media (max-width:500px){h1{font-size:2.4rem}}.gh-input{-webkit-appearance:none;border:1px solid #d6e3eb;border-radius:4px;color:#4b5b62;display:block;font-size:1.6rem;font-weight:300;height:40px;line-height:1em;padding:10px 12px;transition:border-color .15s linear;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;width:100%}.gh-input:focus{border-color:#b4cbda;outline:0}.gh-btn{fill:#829aa8;-webkit-font-smoothing:subpixel-antialiased;border:1px solid #d6e3eb;border-radius:5px;color:#829aa8;display:inline-block;outline:none;text-decoration:none!important;text-shadow:0 1px 0 #fff;transition:all .2s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.gh-btn span{border-radius:4px;display:block;font-size:1.3rem;font-weight:400;height:33px;letter-spacing:.2px;line-height:33px;padding:0 12px;text-align:center}.gh-btn:hover{border-color:#b4cbda}.gh-btn-hover-blue:hover{border-color:#3eb0ef;color:#3eb0ef}.gh-btn-blue{fill:#fff;background:linear-gradient(#3da1d6,#2288bf);border:0;box-shadow:0 1px 0 rgba(0,0,0,.12);color:#fff;padding:1px;text-shadow:0 -1px 0 rgba(0,0,0,.1);transition:none!important}.gh-btn-blue span{background:linear-gradient(#4ab6f0,#2fa5e4 60%,#2fa5e4 90%,#38a9e5);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1)}.gh-btn-blue:active,.gh-btn-blue:focus{background:#1e78a9}.gh-btn-blue:active span,.gh-btn-blue:focus span{background:#29a0e0;box-shadow:none}.gh-btn-block{display:block;width:100%}.gh-input-icon{display:block;position:relative}.gh-input-icon svg{fill:color(var(--midgrey) l(15%));height:14px;left:10px;position:absolute;top:50%;transform:translateY(-7px);width:auto;z-index:2}.gh-input-icon input{padding-left:35px}.gh-app{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;height:100%;overflow:hidden}.gh-viewport{max-height:100%;overflow:hidden}.gh-main,.gh-viewport{-ms-flex-positive:1;display:-ms-flexbox;display:flex;flex-grow:1}.gh-main{background:#fff;overflow-y:auto;position:relative}.gh-flow{-ms-flex-positive:1;-ms-flex-direction:column;flex-direction:column;flex-grow:1;min-height:100%;overflow-y:auto}.gh-flow,.gh-flow-head{display:-ms-flexbox;display:flex}.gh-flow-head{-ms-flex-negative:0;-ms-flex-pack:justify;flex-shrink:0;justify-content:space-between;padding-bottom:20px;padding-top:4vh}.gh-flow-content-wrap{-ms-flex-positive:1;-ms-flex-negative:0;-ms-flex-pack:center;flex-grow:1;flex-shrink:0;justify-content:center;margin:0 5%;padding-bottom:8vh}.gh-flow-back,.gh-flow-content-wrap{-ms-flex-align:center;align-items:center;display:-ms-flexbox;display:flex}.gh-flow-back{border:1px solid transparent;border-radius:4px;color:#7d878a;font-weight:100;left:0;margin:0 0 0 3%;padding:2px 9px 2px 5px;position:absolute;top:0;transition:all .3s ease}.gh-flow-back svg{height:12px;line-height:14px;margin-right:4px}.gh-flow-back svg path{stroke:#7d878a;stroke-width:1.2px}.gh-flow-back:hover{border:1px solid #dae1e3}.gh-flow-back-plain{-ms-flex-align:center;align-items:center;color:#7d878a;display:-ms-flexbox;display:flex;font-weight:300;left:0;margin:0 0 0 3%;padding:2px 9px 2px 5px;position:absolute;text-decoration:none;top:0;transition:all .3s ease}.gh-flow-back-plain svg{height:12px;line-height:14px;margin-right:4px}.gh-flow-back-plain svg path{stroke:#7d878a;stroke-width:1.2px}.gh-flow-back-plain:hover{color:#15212a}.gh-flow-nav{-ms-flex:1;flex:1;position:relative}.gh-flow-content{color:#738a94;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;font-size:1.9rem;font-weight:100;line-height:1.5em;max-width:700px;text-align:center;width:100%}.gh-flow-content .gh-input-icon input{padding-left:35px}.gh-flow-content-unsubscribe{font-weight:300}@media (max-width:500px){.gh-flow-head-unsubscribe{padding-top:2.8vh}.gh-flow-content{font-size:4vw}.gh-flow-content-unsubscribe{font-size:1.8rem;line-height:1.6em}}.gh-flow-content header{margin:0 auto;max-width:520px}.gh-flow-content h1{font-size:4.2rem;font-weight:100}@media (max-width:600px){.gh-flow-content h1{font-size:7vw}}.gh-flow-content .gh-btn{display:block;margin:20px auto 0;max-width:400px}.gh-flow-content .form-group{margin-bottom:2.5rem}.gh-flow-content input{border:1px solid #dae1e3;font-size:1.6rem;font-weight:100;line-height:1.4em;padding:10px}.gh-flow-em{font-weight:500}.gh-signin{background:#f8fbfd;border:1px solid #dae1e3;border-radius:5px;margin:30px auto;max-width:400px;padding:40px;position:relative;text-align:left;width:100%}.gh-signin .form-group{margin-bottom:1.5rem}.gh-signin .gh-btn{margin:0}.error-content{flex-grow:1;justify-content:center;padding:8vw;user-select:text}.error-content,.error-details{align-items:center;display:flex}.error-details{margin-bottom:4rem}.error-ghost{height:115px;margin:15px}@media (max-width:630px){.error-ghost{display:none}}.error-code{color:#c5d2d9;font-size:10vw;font-weight:600;letter-spacing:-.4vw;line-height:.9em;margin:0}.error-description{border:none;color:#54666d;font-size:2.3rem;font-weight:300;line-height:1.3em;margin:0;padding:0}.error-message{align-items:center;display:flex;flex-direction:column;margin:15px}.error-message a{font-size:1.4rem;line-height:1;margin:8px 0}.error-link{background-color:transparent;color:#5ba4e5;text-decoration:none;transition:background .3s,color .3s}.error-stack{background-color:hsla(0,0%,100%,.3);margin:1rem auto;max-width:800px;padding:2rem}.error-stack-list{list-style-type:none;margin:0;padding:0}.error-stack-list li{display:block}.error-stack-list li:before{color:#bbb;content:"\21AA";display:inline-block;font-size:1.2rem;margin-right:.5rem}.error-stack-function{font-weight:700}
|
|
@@ -107,17 +107,15 @@ class BlogIcon {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* Return path for Blog icon without [subdirectory]/content/image prefix
|
|
111
|
-
* Always returns {string} getIconPath
|
|
112
|
-
* @returns {string} physical storage path of icon
|
|
113
110
|
* @description Checks if we have a custom uploaded icon. If no custom uploaded icon
|
|
114
111
|
* exists, we're returning the default `favicon.ico`
|
|
112
|
+
* @returns {string} physical storage path of site icon without [subdirectory]/content/image prefix
|
|
115
113
|
*/
|
|
116
114
|
getIconPath() {
|
|
117
115
|
const blogIcon = this.settingsCache.get('icon');
|
|
118
116
|
|
|
119
117
|
if (blogIcon) {
|
|
120
|
-
return this.storageUtils.
|
|
118
|
+
return this.storageUtils.getLocalImagesStoragePath(blogIcon);
|
|
121
119
|
} else {
|
|
122
120
|
return path.join(this.config.get('paths:publicFilePath'), 'favicon.ico');
|
|
123
121
|
}
|
|
@@ -214,7 +214,7 @@ class ImageSize {
|
|
|
214
214
|
imagePath = this.urlUtils.urlFor('image', {image: imagePath}, true);
|
|
215
215
|
|
|
216
216
|
// get the storage readable filePath
|
|
217
|
-
filePath = this.storageUtils.
|
|
217
|
+
filePath = this.storageUtils.getLocalImagesStoragePath(imagePath);
|
|
218
218
|
|
|
219
219
|
return this.storage.getStorage('images')
|
|
220
220
|
.read({path: filePath})
|
|
@@ -4,10 +4,7 @@ const db = require('../data/db');
|
|
|
4
4
|
const LimitService = require('@tryghost/limit-service');
|
|
5
5
|
let limitService = new LimitService();
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
* @param {Object} [limits] - An object containing limit configuration
|
|
9
|
-
**/
|
|
10
|
-
const initFn = (limits = {}) => {
|
|
7
|
+
const init = () => {
|
|
11
8
|
let helpLink;
|
|
12
9
|
|
|
13
10
|
if (config.get('hostSettings:billing:enabled') && config.get('hostSettings:billing:enabled') === true && config.get('hostSettings:billing:url')) {
|
|
@@ -28,7 +25,7 @@ const initFn = (limits = {}) => {
|
|
|
28
25
|
const hostLimits = config.get('hostSettings:limits') || {};
|
|
29
26
|
|
|
30
27
|
limitService.loadLimits({
|
|
31
|
-
limits:
|
|
28
|
+
limits: hostLimits,
|
|
32
29
|
subscription,
|
|
33
30
|
db,
|
|
34
31
|
helpLink,
|
|
@@ -38,4 +35,4 @@ const initFn = (limits = {}) => {
|
|
|
38
35
|
|
|
39
36
|
module.exports = limitService;
|
|
40
37
|
|
|
41
|
-
module.exports.init =
|
|
38
|
+
module.exports.init = init;
|
|
@@ -80,7 +80,7 @@ function createApiInstance(config) {
|
|
|
80
80
|
return `
|
|
81
81
|
Hey there!
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
Tap the link below to complete the signup process for ${siteTitle}, and be automatically signed in:
|
|
84
84
|
|
|
85
85
|
${url}
|
|
86
86
|
|
|
@@ -107,7 +107,7 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
|
|
|
107
107
|
<div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 600px; padding: 30px 20px;">
|
|
108
108
|
|
|
109
109
|
<!-- START CENTERED WHITE CONTAINER -->
|
|
110
|
-
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">
|
|
110
|
+
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Complete signup for ${siteTitle}!</span>
|
|
111
111
|
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
|
|
112
112
|
|
|
113
113
|
<!-- START MAIN CONTENT AREA -->
|
|
@@ -117,7 +117,7 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
|
|
|
117
117
|
<tr>
|
|
118
118
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
|
119
119
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Hey there!</p>
|
|
120
|
-
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 32px;">
|
|
120
|
+
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 32px;">Tap the link below to complete the signup process for ${siteTitle}, and be automatically signed in:</p>
|
|
121
121
|
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
|
|
122
122
|
<tbody>
|
|
123
123
|
<tr>
|
|
@@ -4,6 +4,9 @@ const {Buffer} = require('buffer');
|
|
|
4
4
|
const {randomBytes} = require('crypto');
|
|
5
5
|
const {URL} = require('url');
|
|
6
6
|
|
|
7
|
+
const config = require('../../../shared/config');
|
|
8
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
9
|
+
|
|
7
10
|
const messages = {
|
|
8
11
|
incorrectState: 'State did not match.'
|
|
9
12
|
};
|
|
@@ -24,6 +27,7 @@ const redirectURI = 'https://stripe.ghost.org';
|
|
|
24
27
|
* @returns {Promise<URL>}
|
|
25
28
|
*/
|
|
26
29
|
async function getStripeConnectOAuthUrl(setSessionProp, mode = 'live') {
|
|
30
|
+
checkCanConnect();
|
|
27
31
|
const randomState = randomBytes(16).toString('hex');
|
|
28
32
|
const state = Buffer.from(JSON.stringify({
|
|
29
33
|
mode,
|
|
@@ -71,6 +75,16 @@ async function getStripeConnectTokenData(encodedData, getSessionProp) {
|
|
|
71
75
|
};
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
function checkCanConnect() {
|
|
79
|
+
const siteUrl = urlUtils.getSiteUrl();
|
|
80
|
+
const productionMode = config.get('env') === 'production';
|
|
81
|
+
const siteUrlUsingSSL = /^https/.test(siteUrl);
|
|
82
|
+
const cannotConnectToStripe = productionMode && !siteUrlUsingSSL;
|
|
83
|
+
if (cannotConnectToStripe) {
|
|
84
|
+
throw new errors.BadRequestError('Cannot connect to stripe unless site is using https://');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
module.exports = {
|
|
75
89
|
getStripeConnectOAuthUrl,
|
|
76
90
|
getStripeConnectTokenData,
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
const labs = require('../../../shared/labs');
|
|
2
|
-
const events = require('../../lib/common/events');
|
|
3
|
-
|
|
4
1
|
const DynamicRedirectManager = require('@tryghost/express-dynamic-redirects');
|
|
5
2
|
const OffersModule = require('@tryghost/members-offers');
|
|
6
3
|
|
|
@@ -28,34 +25,7 @@ module.exports = {
|
|
|
28
25
|
|
|
29
26
|
this.api = offersModule.api;
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
if (labs.isSet('offers')) {
|
|
33
|
-
// handles setting up redirects
|
|
34
|
-
const promise = offersModule.init();
|
|
35
|
-
initCalled = true;
|
|
36
|
-
await promise;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// TODO: Delete after GA
|
|
40
|
-
let offersEnabled = labs.isSet('offers');
|
|
41
|
-
events.on('settings.labs.edited', async () => {
|
|
42
|
-
if (labs.isSet('offers') && !initCalled) {
|
|
43
|
-
const promise = offersModule.init();
|
|
44
|
-
initCalled = true;
|
|
45
|
-
await promise;
|
|
46
|
-
} else if (labs.isSet('offers') !== offersEnabled) {
|
|
47
|
-
offersEnabled = labs.isSet('offers');
|
|
48
|
-
|
|
49
|
-
if (offersEnabled) {
|
|
50
|
-
const offers = await this.api.listOffers({});
|
|
51
|
-
for (const offer of offers) {
|
|
52
|
-
redirectManager.addRedirect(`/${offer.code}`, `/#/portal/offers/${offer.id}`, {permanent: false});
|
|
53
|
-
}
|
|
54
|
-
} else {
|
|
55
|
-
redirectManager.removeAllRedirects();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
});
|
|
28
|
+
await offersModule.init();
|
|
59
29
|
},
|
|
60
30
|
|
|
61
31
|
api: null,
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const moment = require('moment-timezone');
|
|
4
|
+
const yaml = require('js-yaml');
|
|
5
|
+
|
|
6
|
+
const logging = require('@tryghost/logging');
|
|
7
|
+
const tpl = require('@tryghost/tpl');
|
|
8
|
+
const errors = require('@tryghost/errors');
|
|
9
|
+
|
|
10
|
+
const validation = require('./validation');
|
|
11
|
+
|
|
12
|
+
const messages = {
|
|
13
|
+
jsonParse: 'Could not parse JSON: {context}.',
|
|
14
|
+
yamlParse: 'Could not parse YAML: {context}.',
|
|
15
|
+
yamlPlainString: 'YAML input cannot be a plain string. Check the format of your YAML file.',
|
|
16
|
+
redirectsHelp: 'https://ghost.org/docs/themes/routing/#redirects',
|
|
17
|
+
redirectsRegister: 'Could not register custom redirects.'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Redirect configuration object
|
|
22
|
+
* @typedef {Object} RedirectConfig
|
|
23
|
+
* @property {String} from - Defines the relative incoming URL or pattern (regex)
|
|
24
|
+
* @property {String} to - Defines where the incoming traffic should be redirected to, which can be a static URL, or a dynamic value using regex (example: "to": "/$1/")
|
|
25
|
+
* @property {boolean} permanent - Can be defined with true for a permanent HTTP 301 redirect, or false for a temporary HTTP 302 redirect
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} redirectsPath
|
|
30
|
+
* @returns {Promise<string>}
|
|
31
|
+
*/
|
|
32
|
+
const readRedirectsFile = async (redirectsPath) => {
|
|
33
|
+
try {
|
|
34
|
+
return await fs.readFile(redirectsPath, 'utf-8');
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (err.code === 'ENOENT') {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (errors.utils.isIgnitionError(err)) {
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw new errors.NotFoundError({
|
|
45
|
+
err: err
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
*
|
|
52
|
+
* @param {String} content serialized JSON or YAML configuration
|
|
53
|
+
* @param {String} ext one of `.json` or `.yaml` extensions
|
|
54
|
+
*
|
|
55
|
+
* @returns {RedirectConfig[]} of parsed redirect config objects
|
|
56
|
+
*/
|
|
57
|
+
const parseRedirectsFile = (content, ext) => {
|
|
58
|
+
if (ext === '.json') {
|
|
59
|
+
let redirects;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
redirects = JSON.parse(content);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
throw new errors.BadRequestError({
|
|
65
|
+
message: tpl(messages.jsonParse, {context: err.message})
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return redirects;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (ext === '.yaml') {
|
|
73
|
+
let redirects = [];
|
|
74
|
+
let configYaml;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
configYaml = yaml.load(content);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
throw new errors.BadRequestError({
|
|
80
|
+
message: tpl(messages.yamlParse, {context: err.message})
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// yaml.load passes almost every yaml code.
|
|
85
|
+
// Because of that, it's hard to detect if there's an error in the file.
|
|
86
|
+
// But one of the obvious errors is the plain string output.
|
|
87
|
+
// Here we check if the user made this mistake.
|
|
88
|
+
if (typeof configYaml === 'string') {
|
|
89
|
+
throw new errors.BadRequestError({
|
|
90
|
+
message: tpl(messages.yamlPlainString),
|
|
91
|
+
help: tpl(messages.redirectsHelp)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 302: Temporary redirects
|
|
97
|
+
*/
|
|
98
|
+
for (const redirect in configYaml['302']) {
|
|
99
|
+
redirects.push({
|
|
100
|
+
from: redirect,
|
|
101
|
+
to: configYaml['302'][redirect],
|
|
102
|
+
permanent: false
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 301: Permanent redirects
|
|
108
|
+
*/
|
|
109
|
+
for (const redirect in configYaml['301']) {
|
|
110
|
+
redirects.push({
|
|
111
|
+
from: redirect,
|
|
112
|
+
to: configYaml['301'][redirect],
|
|
113
|
+
permanent: true
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return redirects;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new errors.IncorrectUsageError();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} filePath
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
const getBackupRedirectsFilePath = (filePath) => {
|
|
128
|
+
const {dir, name, ext} = path.parse(filePath);
|
|
129
|
+
|
|
130
|
+
return path.join(dir, `${name}-${moment().format('YYYY-MM-DD-HH-mm-ss')}${ext}`);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @typedef {object} IRedirectManager
|
|
135
|
+
*/
|
|
136
|
+
|
|
137
|
+
class CustomRedirectsAPI {
|
|
138
|
+
/**
|
|
139
|
+
* @param {object} config
|
|
140
|
+
* @param {string} config.basePath
|
|
141
|
+
*
|
|
142
|
+
* @param {IRedirectManager} redirectManager
|
|
143
|
+
*/
|
|
144
|
+
constructor(config, redirectManager) {
|
|
145
|
+
/** @private */
|
|
146
|
+
this.config = config;
|
|
147
|
+
/** @private */
|
|
148
|
+
this.redirectManager = redirectManager;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async init() {
|
|
152
|
+
// NOTE: the try/catch block here is due to possible breaking change for existing misconfigured
|
|
153
|
+
// instances in the wild. Would be a good idea to remove it during v5 migration to enforce
|
|
154
|
+
// fail-fast initialization.
|
|
155
|
+
try {
|
|
156
|
+
const filePath = await this.getRedirectsFilePath();
|
|
157
|
+
|
|
158
|
+
if (filePath !== null) {
|
|
159
|
+
const content = await readRedirectsFile(filePath);
|
|
160
|
+
const ext = path.extname(filePath);
|
|
161
|
+
const redirects = parseRedirectsFile(content, ext);
|
|
162
|
+
validation.validate(redirects);
|
|
163
|
+
|
|
164
|
+
this.redirectManager.removeAllRedirects();
|
|
165
|
+
for (const redirect of redirects) {
|
|
166
|
+
this.redirectManager.addRedirect(redirect.from, redirect.to, {permanent: redirect.permanent});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (errors.utils.isIgnitionError(err)) {
|
|
171
|
+
logging.error(err);
|
|
172
|
+
} else {
|
|
173
|
+
logging.error(new errors.IncorrectUsageError({
|
|
174
|
+
message: tpl(messages.redirectsRegister),
|
|
175
|
+
context: err.message,
|
|
176
|
+
help: tpl(messages.redirectsHelp),
|
|
177
|
+
err
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @private
|
|
185
|
+
* @param {'.yaml'|'.json'} ext
|
|
186
|
+
*
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
createRedirectsFilePath(ext) {
|
|
190
|
+
return path.join(this.config.basePath, `redirects${ext}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @returns {Promise<string>}
|
|
195
|
+
*/
|
|
196
|
+
async getRedirectsFilePath() {
|
|
197
|
+
const yamlPath = this.createRedirectsFilePath('.yaml');
|
|
198
|
+
const jsonPath = this.createRedirectsFilePath('.json');
|
|
199
|
+
|
|
200
|
+
const yamlExists = await fs.pathExists(yamlPath);
|
|
201
|
+
|
|
202
|
+
if (yamlExists) {
|
|
203
|
+
return yamlPath;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const jsonExist = await fs.pathExists(jsonPath);
|
|
207
|
+
|
|
208
|
+
if (jsonExist) {
|
|
209
|
+
return jsonPath;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {string} filePath
|
|
217
|
+
* @param {'.yaml'|'.json'} [ext]
|
|
218
|
+
*
|
|
219
|
+
* @returns {Promise<>}
|
|
220
|
+
*/
|
|
221
|
+
async setFromFilePath(filePath, ext = '.json') {
|
|
222
|
+
const redirectsFilePath = await this.getRedirectsFilePath();
|
|
223
|
+
|
|
224
|
+
if (redirectsFilePath) {
|
|
225
|
+
const backupRedirectsPath = getBackupRedirectsFilePath(redirectsFilePath);
|
|
226
|
+
|
|
227
|
+
const backupExists = await fs.pathExists(backupRedirectsPath);
|
|
228
|
+
if (backupExists) {
|
|
229
|
+
await fs.unlink(backupRedirectsPath);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await fs.move(redirectsFilePath, backupRedirectsPath);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const content = await readRedirectsFile(filePath);
|
|
236
|
+
const parsed = parseRedirectsFile(content, ext);
|
|
237
|
+
validation.validate(parsed);
|
|
238
|
+
|
|
239
|
+
if (ext === '.json') {
|
|
240
|
+
await fs.writeFile(this.createRedirectsFilePath('.json'), JSON.stringify(content), 'utf-8');
|
|
241
|
+
} else if (ext === '.yaml') {
|
|
242
|
+
await fs.copy(filePath, this.createRedirectsFilePath('.yaml'));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.redirectManager.removeAllRedirects();
|
|
246
|
+
for (const redirect of parsed) {
|
|
247
|
+
this.redirectManager.addRedirect(redirect.from, redirect.to, {permanent: redirect.permanent});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @returns {Promise<RedirectConfig[]>}
|
|
253
|
+
*/
|
|
254
|
+
async get() {
|
|
255
|
+
const filePath = await this.getRedirectsFilePath();
|
|
256
|
+
if (filePath === null) {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const content = await readRedirectsFile(filePath);
|
|
261
|
+
|
|
262
|
+
if (path.extname(filePath) === '.json') {
|
|
263
|
+
return parseRedirectsFile(content, '.json');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return content;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = CustomRedirectsAPI;
|