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:
commit
7b6805580c
7 changed files with 92 additions and 13 deletions
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue