Added quotes to NQL filters with ids (#18958)

refs https://github.com/TryGhost/Product/issues/4120

Updated some places where we don't add quotes around ids in NQL filters,
which can be an issue when the id is a number
This commit is contained in:
Simon Backx 2023-11-13 12:00:20 +01:00 committed by GitHub
parent 660f5fef6f
commit 14927ee24b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 42 additions and 42 deletions

View File

@ -124,7 +124,7 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}: {site
browse({page, postId}: {page: number, postId: string}) {
firstCommentsLoadedAt = firstCommentsLoadedAt ?? new Date().toISOString();
const filter = encodeURIComponent(`post_id:${postId}+created_at:<=${firstCommentsLoadedAt}`);
const filter = encodeURIComponent(`post_id:'${postId}'+created_at:<=${firstCommentsLoadedAt}`);
const order = encodeURIComponent('created_at DESC, id DESC');
const url = endpointFor({type: 'members', resource: 'comments', params: `?limit=5&order=${order}&filter=${filter}&page=${page}`});

View File

@ -4,15 +4,15 @@ const FEEDBACK_RELATION_OPTIONS = [
];
export const AUDIENCE_FEEDBACK_FILTER = {
label: 'Responded with feedback',
name: 'newsletter_feedback',
valueType: 'string',
resource: 'email',
label: 'Responded with feedback',
name: 'newsletter_feedback',
valueType: 'string',
resource: 'email',
relationOptions: FEEDBACK_RELATION_OPTIONS,
feature: 'audienceFeedback',
feature: 'audienceFeedback',
buildNqlFilter: (filter) => {
// Added brackets to make sure we can parse as a single AND filter
return `(feedback.post_id:${filter.value}+feedback.score:${filter.relation})`;
return `(feedback.post_id:'${filter.value}'+feedback.score:${filter.relation})`;
},
parseNqlFilter: (filter) => {
if (!filter.$and) {

View File

@ -96,8 +96,8 @@ export default class Analytics extends Component {
const values = [this.post.count.positive_feedback, this.post.count.negative_feedback];
const labels = ['More like this', 'Less like this'];
const links = [
{filterParam: '(feedback.post_id:' + this.post.id + '+feedback.score:1)'},
{filterParam: '(feedback.post_id:' + this.post.id + '+feedback.score:0)'}
{filterParam: '(feedback.post_id:\'' + this.post.id + '\'+feedback.score:1)'},
{filterParam: '(feedback.post_id:\'' + this.post.id + '\'+feedback.score:0)'}
];
const colors = ['#F080B2', '#8452f633'];
return {values, labels, links, colors};
@ -234,7 +234,7 @@ export default class Analytics extends Component {
return link;
});
const filter = `post_id:${this.post.id}+to:'${currentLink}'`;
const filter = `post_id:'${this.post.id}'+to:'${currentLink}'`;
let bulkUpdateUrl = this.ghostPaths.url.api(`links/bulk`) + `?filter=${encodeURIComponent(filter)}`;
yield this.ajax.put(bulkUpdateUrl, {
data: {
@ -246,7 +246,7 @@ export default class Analytics extends Component {
});
// Refresh links data
const linksFilter = `post_id:${this.post.id}`;
const linksFilter = `post_id:'${this.post.id}'`;
let statsUrl = this.ghostPaths.url.api(`links/`) + `?filter=${encodeURIComponent(linksFilter)}`;
let result = yield this.ajax.request(statsUrl);
this.updateLinkData(result.links);
@ -271,7 +271,7 @@ export default class Analytics extends Component {
@task
*_fetchLinks() {
const filter = `post_id:${this.post.id}`;
const filter = `post_id:'${this.post.id}'`;
let statsUrl = this.ghostPaths.url.api(`links/`) + `?filter=${encodeURIComponent(filter)}`;
let result = yield this.ajax.request(statsUrl);
this.updateLinkData(result.links);
@ -286,7 +286,7 @@ export default class Analytics extends Component {
@task
*_fetchMentions() {
const filter = `resource_id:${this.post.id}+resource_type:post`;
const filter = `resource_id:'${this.post.id}'+resource_type:post`;
this.mentions = yield this.store.query('mention', {limit: 5, order: 'created_at desc', filter});
}

View File

@ -204,7 +204,7 @@ export default class Analytics extends Component {
return link;
});
const filter = `post_id:${this.post.id}+to:'${currentLink}'`;
const filter = `post_id:'${this.post.id}'+to:'${currentLink}'`;
let bulkUpdateUrl = this.ghostPaths.url.api(`links/bulk`) + `?filter=${encodeURIComponent(filter)}`;
yield this.ajax.put(bulkUpdateUrl, {
data: {
@ -216,7 +216,7 @@ export default class Analytics extends Component {
});
// Refresh links data
const linksFilter = `post_id:${this.post.id}`;
const linksFilter = `post_id:'${this.post.id}'`;
let statsUrl = this.ghostPaths.url.api(`links/`) + `?filter=${encodeURIComponent(linksFilter)}`;
let result = yield this.ajax.request(statsUrl);
this.updateLinkData(result.links);
@ -241,7 +241,7 @@ export default class Analytics extends Component {
@task
*_fetchLinks() {
const filter = `post_id:${this.post.id}`;
const filter = `post_id:'${this.post.id}'`;
let statsUrl = this.ghostPaths.url.api(`links/`) + `?filter=${encodeURIComponent(filter)}`;
let result = yield this.ajax.request(statsUrl);
this.updateLinkData(result.links);

View File

@ -3,8 +3,8 @@ import Component from '@glimmer/component';
export default class FooterLinks extends Component {
get feedbackLinks() {
const post = this.args.post;
const positiveLink = {filterParam: '(feedback.post_id:' + post.id + '+feedback.score:1)', label: 'More like this'};
const negativeLink = {filterParam: '(feedback.post_id:' + post.id + '+feedback.score:0)', label: 'Less like this'};
const positiveLink = {filterParam: '(feedback.post_id:\'' + post.id + '\'+feedback.score:1)', label: 'More like this'};
const negativeLink = {filterParam: '(feedback.post_id:\'' + post.id + '\'+feedback.score:0)', label: 'Less like this'};
const data = [
{link: positiveLink, hidden: !post.count.positive_feedback},

View File

@ -33,7 +33,7 @@ export default class HistoryEventFilter extends Helper {
}
if (user) {
filterParts.push(`actor_id:${user}`);
filterParts.push(`actor_id:'${user}'`);
}
return filterParts.join('+');

View File

@ -45,11 +45,11 @@ export default class MembersEventFilter extends Helper {
}
if (member) {
filterParts.push(`data.member_id:${member}`);
filterParts.push(`data.member_id:'${member}'`);
}
if (post) {
filterParts.push(`data.post_id:${post}`);
filterParts.push(`data.post_id:'${post}'`);
}
return filterParts.join('+');

View File

@ -39,7 +39,7 @@ export default class MentionsRoute extends AuthenticatedRoute {
let extension = undefined;
if (params.post_id) {
paginationSettings.filter = `resource_id:${params.post_id}+resource_type:post`;
paginationSettings.filter = `resource_id:'${params.post_id}'+resource_type:post`;
} else {
// Only return mentions with the same source once
paginationSettings.unique = true;

View File

@ -78,7 +78,7 @@ const Comment = ghostBookshelf.Model.extend({
// Enforce _blank and safe URLs
transformTags: {
a: sanitizeHtml.simpleTransform('a', {
target: '_blank',
target: '_blank',
rel: 'ugc noopener noreferrer nofollow'
})
}
@ -106,7 +106,7 @@ const Comment = ghostBookshelf.Model.extend({
if (options.parentId === null) {
return 'parent_id:null';
}
return 'parent_id:' + options.parentId;
return 'parent_id:\'' + options.parentId + '\'';
}
return null;

View File

@ -904,7 +904,7 @@ Post = ghostBookshelf.Model.extend({
ops.push(function updateRevisions() {
return ghostBookshelf.model('MobiledocRevision')
.findAll(Object.assign({
filter: `post_id:${model.id}`,
filter: `post_id:'${model.id}'`,
columns: ['id']
}, _.pick(options, 'transacting')))
.then((revisions) => {
@ -958,7 +958,7 @@ Post = ghostBookshelf.Model.extend({
ops.push(async function updateRevisions() {
const revisionModels = await ghostBookshelf.model('PostRevision')
.findAll(Object.assign({
filter: `post_id:${model.id}`,
filter: `post_id:'${model.id}'`,
columns: ['id', 'lexical', 'created_at', 'author_id', 'title', 'reason', 'post_status', 'created_at_ts', 'feature_image']
}, _.pick(options, 'transacting')));

View File

@ -616,7 +616,7 @@ describe('Comments API', function () {
.expectEmptyBody();
// Check report
const reports = await models.CommentReport.findAll({filter: 'comment_id:' + commentId});
const reports = await models.CommentReport.findAll({filter: 'comment_id:\'' + commentId + '\''});
reports.models.length.should.eql(1);
const report = reports.models[0];
@ -641,7 +641,7 @@ describe('Comments API', function () {
.expectEmptyBody();
// Check report should be the same (no extra created)
const reports = await models.CommentReport.findAll({filter: 'comment_id:' + commentId});
const reports = await models.CommentReport.findAll({filter: 'comment_id:\'' + commentId + '\''});
reports.models.length.should.eql(1);
const report = reports.models[0];

View File

@ -567,7 +567,7 @@ describe('Batch sending tests', function () {
// Test if all links are replaced and contain the member id
const cheerio = require('cheerio');
const $ = cheerio.load(html);
const links = await linkRedirectRepository.getAll({filter: 'post_id:' + emailModel.get('post_id')});
const links = await linkRedirectRepository.getAll({filter: 'post_id:\'' + emailModel.get('post_id') + '\''});
for (const el of $('a').toArray()) {
const href = $(el).attr('href');
@ -612,7 +612,7 @@ describe('Batch sending tests', function () {
const {emailModel, html} = await sendEmail(agent);
assert.match(html, /\m=/);
const links = await linkRedirectRepository.getAll({filter: 'post_id:' + emailModel.get('post_id')});
const links = await linkRedirectRepository.getAll({filter: 'post_id:\'' + emailModel.get('post_id') + '\''});
for (const link of links) {
// Check ref not added to all replaced links
@ -627,7 +627,7 @@ describe('Batch sending tests', function () {
const {emailModel, html} = await sendEmail(agent);
assert.match(html, /\m=/);
const links = await linkRedirectRepository.getAll({filter: 'post_id:' + emailModel.get('post_id')});
const links = await linkRedirectRepository.getAll({filter: 'post_id:\'' + emailModel.get('post_id') + '\''});
for (const link of links) {
// Check ref not added to all replaced links
@ -640,7 +640,7 @@ describe('Batch sending tests', function () {
const {emailModel, html} = await sendEmail(agent);
assert.doesNotMatch(html, /\m=/);
const links = await linkRedirectRepository.getAll({filter: 'post_id:' + emailModel.get('post_id')});
const links = await linkRedirectRepository.getAll({filter: 'post_id:\'' + emailModel.get('post_id') + '\''});
assert.equal(links.length, 0);
});
});

View File

@ -216,7 +216,7 @@ class BatchSendingService {
async getBatches(email) {
logging.info(`Getting batches for email ${email.id}`);
return await this.#models.EmailBatch.findAll({filter: 'email_id:' + email.id});
return await this.#models.EmailBatch.findAll({filter: 'email_id:\'' + email.id + '\''});
}
/**
@ -501,7 +501,7 @@ class BatchSendingService {
* @returns {Promise<MemberLike[]>}
*/
async getBatchMembers(batchId) {
let models = await this.#models.EmailRecipient.findAll({filter: `batch_id:${batchId}`, withRelated: ['member', 'member.stripeSubscriptions', 'member.products']});
let models = await this.#models.EmailRecipient.findAll({filter: `batch_id:'${batchId}'`, withRelated: ['member', 'member.stripeSubscriptions', 'member.products']});
const BATCH_SIZE = this.#sendingService.getMaximumRecipients();
if (models.length > BATCH_SIZE) {

View File

@ -26,7 +26,7 @@ class EmailSegmenter {
}
getMemberFilterForSegment(newsletter, emailRecipientFilter, segment) {
const filter = [`newsletters.id:${newsletter.id}`, 'email_disabled:0'];
const filter = [`newsletters.id:'${newsletter.id}'`, 'email_disabled:0'];
switch (emailRecipientFilter) {
case 'all':

View File

@ -37,7 +37,7 @@ describe('Email segmenter', function () {
);
listStub.calledOnce.should.be.true();
listStub.calledWith({
filter: 'newsletters.id:newsletter-123+email_disabled:0'
filter: 'newsletters.id:\'newsletter-123\'+email_disabled:0'
}).should.be.true();
response.should.eql(12);
});
@ -94,7 +94,7 @@ describe('Email segmenter', function () {
listStub.calledOnce.should.be.true();
listStub.calledWith({
filter: 'newsletters.id:newsletter-123+email_disabled:0+(labels:test)+status:-free'
filter: 'newsletters.id:\'newsletter-123\'+email_disabled:0+(labels:test)+status:-free'
}).should.be.true();
response.should.eql(12);
});
@ -118,7 +118,7 @@ describe('Email segmenter', function () {
listStub.calledOnce.should.be.true();
listStub.calledWith({
filter: 'newsletters.id:newsletter-123+email_disabled:0+(labels:test)+(status:free)'
filter: 'newsletters.id:\'newsletter-123\'+email_disabled:0+(labels:test)+(status:free)'
}).should.be.true();
response.should.eql(12);
});

View File

@ -154,7 +154,7 @@ class LinkClickTrackingService {
// manages transformation of current url to relative for comparision
const transformedOldUrl = this.#urlUtils.absoluteToTransformReady(redirectUrl.href);
const filterQuery = `post_id:${postId}+to:'${transformedOldUrl}'`;
const filterQuery = `post_id:'${postId}'+to:'${transformedOldUrl}'`;
const updatedFilterOptions = {
...filterOptions,

View File

@ -764,9 +764,9 @@ module.exports = class MemberRepository {
if (data.action === 'unsubscribe') {
const hasNewsletterSelected = (Object.prototype.hasOwnProperty.call(data, 'newsletter') && data.newsletter !== null);
if (hasNewsletterSelected) {
const membersArr = memberIds.join(',');
const membersArr = memberIds.map(i => `'${i}'`).join(',');
const unsubscribeRows = await this._MemberNewsletter.getFilteredCollectionQuery({
filter: `newsletter_id:${data.newsletter}+member_id:[${membersArr}]`
filter: `newsletter_id:'${data.newsletter}'+member_id:[${membersArr}]`
});
const toUnsubscribe = unsubscribeRows.map(row => row.id);

View File

@ -127,7 +127,7 @@ export class PostRevisions {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async removeAuthorFromRevisions(authorId: string, options: any): Promise<void> {
const revisions = await this.model.findAll({
filter: `author_id:${authorId}`,
filter: `author_id:'${authorId}'`,
columns: ['id'],
transacting: options.transacting
});