Skip to content

Commit

Permalink
Merge branch 'development' of https://github.com/huridocs/uwazi into …
Browse files Browse the repository at this point in the history
…3.1.4-fix
  • Loading branch information
Zasa-san committed Jun 13, 2024
2 parents 8fcef4b + c87f5e7 commit 3f921da
Show file tree
Hide file tree
Showing 35 changed files with 1,730 additions and 589 deletions.
16 changes: 10 additions & 6 deletions app/api/activitylog/activityLogBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export enum Methods {
enum Methods {
Create = 'CREATE',
Update = 'UPDATE',
Delete = 'DELETE',
Expand All @@ -12,7 +12,7 @@ const buildActivityLogEntry = (builder: ActivityLogBuilder) => ({
...(builder.extra && { extra: builder.extra }),
});

export interface EntryValue {
interface EntryValue {
idField?: string;
nameField?: string;
id?: any;
Expand All @@ -23,12 +23,12 @@ export interface EntryValue {
desc: string;
}

export interface LogActivity {
interface LogActivity {
name?: string;
[k: string]: any | undefined;
}

export class ActivityLogBuilder {
class ActivityLogBuilder {
description: string;

action: Methods;
Expand Down Expand Up @@ -89,7 +89,8 @@ const changeToUpdate = (entryValue: EntryValue): EntryValue => {

function checkForUpdate(body: any, entryValue: EntryValue) {
const content = JSON.parse(body);
const id = entryValue.idField ? content[entryValue.idField] : null;
const json = content.entity ? JSON.parse(content.entity) : content;
const id = entryValue.idField ? json[entryValue.idField] : null;
let activityInput = { ...entryValue };
if (id && entryValue.method !== Methods.Delete) {
activityInput = changeToUpdate(entryValue);
Expand All @@ -103,7 +104,7 @@ const getActivityInput = (entryValue: EntryValue, body: any) => {
return idPost ? checkForUpdate(body, entryValue) : entryValue;
};

export const buildActivityEntry = async (entryValue: EntryValue, data: any) => {
const buildActivityEntry = async (entryValue: EntryValue, data: any) => {
const body = data.body && data.body !== '{}' ? data.body : data.query || '{}';
const activityInput = getActivityInput(entryValue, body);
const activityEntryBuilder = new ActivityLogBuilder(JSON.parse(body), activityInput);
Expand All @@ -112,3 +113,6 @@ export const buildActivityEntry = async (entryValue: EntryValue, data: any) => {
activityEntryBuilder.makeExtra();
return activityEntryBuilder.build();
};

export type { ActivityLogBuilder, EntryValue, LogActivity };
export { Methods, buildActivityEntry };
228 changes: 228 additions & 0 deletions app/api/activitylog/activityLogFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { isEmpty } from 'lodash';
import { FilterQuery } from 'mongoose';
import moment from 'moment';
import { ActivityLogGetRequest } from 'shared/types/activityLogApiTypes';
import { ActivityLogEntryType } from 'shared/types/activityLogEntryType';
import { escapeEspecialChars } from 'shared/data_utils/stringUtils';
import { ParsedActions } from './activitylogParser';
import { EntryValue } from './activityLogBuilder';

type ActivityLogQuery = Required<ActivityLogGetRequest>['query'];
type ActivityLogQueryTime = Required<ActivityLogQuery>['time'];
const prepareToFromRanges = (sanitizedTime: ActivityLogQueryTime) => {
const fromDate = sanitizedTime.from && new Date(sanitizedTime.from);
const toDate = sanitizedTime.to && moment(new Date(sanitizedTime.to)).add(1, 'day').toDate();
return {
...(fromDate && { $gte: fromDate.getTime() }),
...(toDate && { $lt: toDate.getTime() }),
};
};
const parsedActionsEntries = Object.entries(ParsedActions);
const queryURL = (matchedEntries: [string, EntryValue][]) =>
matchedEntries.map(([key]) => {
const entries = key.split(/\/(.*)/s);
const condition = {
$and: [
{ url: { $regex: `^\\/${escapeEspecialChars(entries[1])}$` } },
{ method: entries[0] },
],
};
return condition;
});
const andCondition = (method: string, regex: string, property: string = 'body') => ({
$and: [{ method }, { [property]: { $regex: regex } }],
});
const bodyCondition = (methods: string[]) => {
const orContent: FilterQuery<ActivityLogEntryType>[] = [];
methods.forEach(method => {
switch (method) {
case 'CREATE':
orContent.push({
$and: [
andCondition('POST', '^(?!{"_id").*'),
andCondition('POST', '^(?!{"entity":"{\\\\"_id).*'),
],
});
break;
case 'UPDATE':
orContent.push(andCondition('POST', '^({"_id").*'));
orContent.push(andCondition('POST', '^({"entity":"{\\\\"_id).*'));
orContent.push(andCondition('PUT', '^({"_id").*'));
orContent.push(andCondition('PUT', '^({"entity":"{\\\\"_id).*'));
break;
case 'DELETE':
orContent.push(andCondition('DELETE', '^({"_id").*'));
orContent.push(andCondition('DELETE', '^({"_id").*', 'query'));
orContent.push(andCondition('DELETE', '^({"sharedId").*', 'query'));
break;
default:
orContent.push({ method });
break;
}
});
return orContent;
};
const sanitizeTime = (time: ActivityLogQueryTime) => (memo: {}, k: string) =>
time[k] !== null ? Object.assign(memo, { [k]: time[k] }) : memo;
const equivalentHttpMethod = (method: string): string =>
['CREATE', 'UPDATE'].includes(method.toUpperCase()) ? 'POST' : method.toUpperCase();
const matchWithParsedEntry = (
key: string,
queryMethods: string[],
value: EntryValue,
methods: string[]
) =>
key.toUpperCase().match(`(${queryMethods.join('|')}).*`) &&
((value.method || '').toUpperCase().match(`(${methods.join('|')}).*`) ||
value.desc.toUpperCase().match(`(${methods.join('|')}).*`));
const reduceUniqueCondition: (
condition: FilterQuery<ActivityLogEntryType>
) => FilterQuery<ActivityLogEntryType> = (condition: FilterQuery<ActivityLogEntryType>) => {
const keys = Object.keys(condition);
return keys.reduce((memo, key) => {
if (['$and', '$or'].includes(key) && condition[key]?.length === 1) {
const reducedCondition = reduceUniqueCondition(condition[key][0]);
return { ...memo, ...reducedCondition };
}
return { ...memo, [key]: condition[key] };
}, {});
};
class ActivityLogFilter {
andQuery: FilterQuery<ActivityLogEntryType>[] = [];

searchQuery: FilterQuery<ActivityLogEntryType>[] = [];

query: ActivityLogQuery;

constructor(requestQuery: ActivityLogQuery) {
const methodFilter = ((requestQuery || {}).method || []).map(method => method.toUpperCase());
this.query = { ...requestQuery, method: methodFilter };
}

timeQuery() {
const { time = {}, before = -1 } = this.query || {};
const sanitizedTime: ActivityLogQueryTime = Object.keys(time).reduce(sanitizeTime(time), {});
if (before === -1 && isEmpty(sanitizedTime)) {
return;
}
const timeFilter = {
...(!isEmpty(sanitizedTime) ? { time: prepareToFromRanges(sanitizedTime) } : {}),
...(before !== -1 ? { time: { $lt: before } } : {}),
};
this.andQuery.push(timeFilter);
}

setRequestFilter(property: 'url' | 'query' | 'body' | 'params', exact = false) {
const filterValue = (this.query || {})[property];
if (filterValue !== undefined) {
const exp = escapeEspecialChars(filterValue);
this.andQuery.push({ [property]: { $regex: exact ? `^${exp}$` : exp } });
}
}

prepareRegexpQueries = () => {
this.setRequestFilter('url', true);
this.setRequestFilter('query');
this.setRequestFilter('body');
this.setRequestFilter('params');
};

searchFilter() {
const { search } = this.query || {};
if (search !== undefined) {
const regex = { $regex: `.*${escapeEspecialChars(search)}.*`, $options: 'si' };
this.searchQuery.push({
$or: [
{
method: {
$regex: `${escapeEspecialChars(search.toUpperCase().replace('CREATE', 'POST'))}`,
},
},
{ url: { $regex: `^${escapeEspecialChars(search)}$` } },
{ query: regex },
{ body: regex },
{ params: regex },
],
});
}
}

methodFilter() {
const { method: methods = [] } = this.query || {};
if (methods.length > 0) {
const queryMethods = methods.map(equivalentHttpMethod);
const matchedEntries = parsedActionsEntries.filter(([key, value]) =>
matchWithParsedEntry(key, queryMethods, value, methods)
);
const bodyTerm = bodyCondition(methods);
if (bodyTerm.length > 0) {
this.andQuery.push({ $or: bodyTerm });
}
if (matchedEntries.length > 0) {
const orUrlItems = queryURL(matchedEntries);
this.andQuery.push({ $or: orUrlItems });
}
}
}

openSearchFilter() {
const { search } = this.query || {};
if (search === undefined) {
return;
}
const matchedURLs = parsedActionsEntries.filter(([_key, value]) =>
value.desc.toLowerCase().includes(search.toLocaleLowerCase() || '')
);
const orUrlItems = queryURL(matchedURLs);
if (matchedURLs.length > 0) {
this.searchQuery.push({ $or: orUrlItems });
} else {
this.searchFilter();
}
if (this.searchQuery.length > 0) {
this.andQuery.push({ $or: this.searchQuery });
}
}

findFilter() {
const { find } = this.query || {};
if (find) {
const regex = { $regex: `.*${escapeEspecialChars(find)}.*`, $options: 'si' };
this.andQuery.push({
$or: [
{ method: regex },
{ url: regex },
{ query: regex },
{ body: regex },
{ params: regex },
],
});
}
}

userFilter() {
const { username } = this.query || {};
if (username) {
const orUser: FilterQuery<ActivityLogEntryType>[] = [
{ username: { $regex: `.*${escapeEspecialChars(username)}.*`, $options: 'si' } },
];
if (username === 'anonymous') {
orUser.push({ username: null });
}
this.andQuery.push({ $or: orUser });
}
}

prepareQuery() {
this.prepareRegexpQueries();
this.methodFilter();
this.openSearchFilter();
this.findFilter();
this.userFilter();
this.timeQuery();
const rootQuery = !isEmpty(this.andQuery) ? reduceUniqueCondition({ $and: this.andQuery }) : {};
return rootQuery;
}
}
export type { ActivityLogQueryTime };
export { ActivityLogFilter, prepareToFromRanges, bodyCondition };
70 changes: 2 additions & 68 deletions app/api/activitylog/activitylog.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sortingParams } from 'shared/types/activityLogApiSchemas';
import model from './activitylogModel';
import { getSemanticData } from './activitylogParser';
import { ActivityLogFilter } from './activityLogFilter';

const sortingParamsAsSet = new Set(sortingParams);

Expand All @@ -12,68 +13,6 @@ const validateSortingParam = param => {
}
};

const prepareRegexpQueries = query => {
const result = {};

if (query.url) {
result.url = new RegExp(query.url);
}
if (query.query) {
result.query = new RegExp(query.query);
}
if (query.body) {
result.body = new RegExp(query.body);
}
if (query.params) {
result.params = new RegExp(query.params);
}

return result;
};

const prepareQuery = query => {
if (!query.find) {
return prepareRegexpQueries(query);
}
const term = new RegExp(query.find);
return {
$or: [{ method: term }, { url: term }, { query: term }, { body: term }, { params: term }],
};
};

const prepareToFromRanges = sanitizedTime => {
const time = {};

if (sanitizedTime.from) {
time.$gte = parseInt(sanitizedTime.from, 10) * 1000;
}

if (sanitizedTime.to) {
time.$lte = parseInt(sanitizedTime.to, 10) * 1000;
}

return time;
};

const timeQuery = ({ time = {}, before = null }) => {
const sanitizedTime = Object.keys(time).reduce(
(memo, k) => (time[k] !== null ? Object.assign(memo, { [k]: time[k] }) : memo),
{}
);

if (before === null && !Object.keys(sanitizedTime).length) {
return {};
}

const result = { time: prepareToFromRanges(sanitizedTime) };

if (before !== null) {
result.time.$lt = parseInt(before, 10);
}

return result;
};

const getPagination = query => {
const { page } = query;
const limit = parseInt(query.limit || 15, 10);
Expand Down Expand Up @@ -112,12 +51,7 @@ export default {
isValidSortingParam,

async get(query = {}) {
const mongoQuery = Object.assign(prepareQuery(query), timeQuery(query));

if (query.method && query.method.length) {
mongoQuery.method = { $in: query.method };
}

const mongoQuery = new ActivityLogFilter(query).prepareQuery();
if (query.username) {
mongoQuery.username =
query.username !== 'anonymous' ? query.username : { $in: [null, query.username] };
Expand Down
6 changes: 3 additions & 3 deletions app/api/activitylog/activitylogParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as helpers from 'api/activitylog/helpers';
import { nameFunc } from 'api/activitylog/helpers';
import { buildActivityEntry, Methods, EntryValue } from 'api/activitylog/activityLogBuilder';

const entryValues: { [key: string]: EntryValue } = {
const ParsedActions: { [key: string]: EntryValue } = {
'POST/api/users': {
desc: 'Updated user',
method: Methods.Update,
Expand Down Expand Up @@ -226,7 +226,7 @@ const getSemanticData = async (data: any) => {
if (action === 'MIGRATE') {
return helpers.migrationLog(data);
}
const entryValue = entryValues[action] || {
const entryValue = ParsedActions[action] || {
desc: '',
extra: () => `${data.method}: ${data.url}`,
method: 'RAW',
Expand All @@ -245,4 +245,4 @@ const getSemanticData = async (data: any) => {
return { ...activityEntry };
};

export { getSemanticData };
export { getSemanticData, ParsedActions };
Loading

0 comments on commit 3f921da

Please sign in to comment.