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

Merged v5.42.1 into main

This commit is contained in:
Ghost CI 2023-04-07 08:51:06 +01:00
commit 7b6805580c
7 changed files with 92 additions and 13 deletions

View file

@ -107,12 +107,10 @@
<div>
<h4 class="gh-expandable-title">Export post analytics</h4>
<p class="gh-expandable-description">
Download a CSV file of all your post data for easy analysis in one place
Download a CSV file of your last 1000 posts data for easy analysis in one place
</p>
</div>
<button type="button" class="gh-btn" {{on "click" this.exportData}}>
<span>Export</span>
</button>
<GhTaskButton @buttonText="Export" @successText="Exported" @task={{this.exportPostsTask}} @class="gh-btn gh-btn-icon"/>
</div>
</div>
</section>

View file

@ -2,6 +2,7 @@ import Component from '@glimmer/component';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
export default class Analytics extends Component {
@service settings;
@ -16,13 +17,15 @@ export default class Analytics extends Component {
this.settings.emailTrackOpens = !this.settings.emailTrackOpens;
}
@action
exportData() {
@task
*exportPostsTask() {
let exportUrl = ghostPaths().url.api('posts/export');
let downloadParams = new URLSearchParams();
downloadParams.set('limit', 'all');
downloadParams.set('limit', 1000);
this.utils.downloadFile(`${exportUrl}?${downloadParams.toString()}`);
yield this.utils.fetchAndDownloadFile(`${exportUrl}?${downloadParams.toString()}`);
return true;
}
@action

View file

@ -1,4 +1,5 @@
import Service from '@ember/service';
import fetch from 'fetch';
export default class UtilsService extends Service {
downloadFile(url) {
@ -14,6 +15,29 @@ export default class UtilsService extends Service {
iframe.setAttribute('src', url);
}
/**
* This method will fetch a file from the server and then download it, resolving
* once the initial fetch is complete, allowing it to be used with loading spinners.
*
* @param {string} url - The URL of the file to download
* @returns {Promise<void>}
*/
async fetchAndDownloadFile(url) {
const response = await fetch(url);
const blob = await response.blob();
const anchor = document.createElement('a');
anchor.href = window.URL.createObjectURL(blob);
anchor.download = /filename="(.*)"/.exec(response.headers.get('Content-Disposition'))[1];
document.body.append(anchor);
anchor.click();
document.body.removeChild(anchor);
}
/**
* Remove tracking parameters from a URL
* @param {string} url

View file

@ -1,6 +1,6 @@
{
"name": "ghost-admin",
"version": "5.42.0",
"version": "5.42.1",
"description": "Ember.js admin client for Ghost",
"author": "Ghost Foundation",
"homepage": "http://ghost.org",

View file

@ -14,15 +14,45 @@ function isDeniedFile(file) {
return deniedFiles.includes(base) || deniedFileTypes.includes(ext);
}
/**
* Copy from:
* https://github.com/pillarjs/send/blob/b69cbb3dc4c09c37917d08a4c13fcd1bac97ade5/index.js#L987-L1003
*
* Allows V8 to only deoptimize this fn instead of all
* of send().
*
* @param {string} filePath
* @returns {string|number} returns -1 number if decode decodeURIComponent throws
*/
function decode(filePath) {
try {
return decodeURIComponent(filePath);
} catch (err) {
return -1;
}
}
/**
*
* @param {string} file path to a requested file
* @returns {boolean}
*/
function isAllowedFile(file) {
const decodedFilePath = decode(file);
if (decodedFilePath === -1) {
return false;
}
const normalizedFilePath = path.normalize(decodedFilePath);
const allowedFiles = ['manifest.json'];
const allowedPath = '/assets/';
const alwaysDeny = ['.hbs'];
const ext = path.extname(file);
const base = path.basename(file);
const ext = path.extname(normalizedFilePath);
const base = path.basename(normalizedFilePath);
return allowedFiles.includes(base) || (file.startsWith(allowedPath) && !alwaysDeny.includes(ext));
return allowedFiles.includes(base) || (normalizedFilePath.startsWith(allowedPath) && !alwaysDeny.includes(ext));
}
function forwardToExpressStatic(req, res, next) {

View file

@ -1,6 +1,6 @@
{
"name": "ghost",
"version": "5.42.0",
"version": "5.42.1",
"description": "The professional publishing platform",
"author": "Ghost Foundation",
"homepage": "https://ghost.org",

View file

@ -184,4 +184,28 @@ describe('staticTheme', function () {
done();
});
});
it('should disallow path traversal', function (done) {
req.path = '/assets/built%2F..%2F..%2F/package.json';
req.method = 'GET';
staticTheme()(req, res, function next() {
activeThemeStub.called.should.be.false();
expressStaticStub.called.should.be.false();
done();
});
});
it('should not crash when malformatted URL sequence is passed', function (done) {
req.path = '/assets/built%2F..%2F..%2F%E0%A4%A/package.json';
req.method = 'GET';
staticTheme()(req, res, function next() {
activeThemeStub.called.should.be.false();
expressStaticStub.called.should.be.false();
done();
});
});
});