Skip to content

Commit

Permalink
feat(web-server): シンプルログ出力でユーザー名と書き込み日時を含めない設定を追加
Browse files Browse the repository at this point in the history
  • Loading branch information
kizahasi committed Jan 15, 2022
1 parent c7a45f3 commit 0822d01
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
generateAsStaticHtml,
} from '../../../../utils/message/roomLogGenerator';
import moment from 'moment';
import { Button, Modal, Progress, Radio } from 'antd';
import { Button, Checkbox, Modal, Progress, Radio } from 'antd';
import classNames from 'classnames';
import { flex, flexColumn } from '../../../../utils/className';
import { useApolloClient } from '@apollo/client';
Expand Down Expand Up @@ -63,6 +63,11 @@ export const GenerateLogModal: React.FC<Props> = ({ roomId, visible, onClose }:
);
const channelsFilterOptionsRef = useReadonlyRef(channelsFilterOptions);

const [showCreatedAt, setShowCreatedAt] = React.useState(true);
const showCreatedAtRef = useReadonlyRef(showCreatedAt);
const [showUsernameAlways, setShowUsernameAlways] = React.useState(true);
const showUsernameAlwaysRef = useReadonlyRef(showUsernameAlways);

const [progress, setProgress] = React.useState<number>();
const [errorMessage, setErrorMessage] = React.useState<string>();
React.useEffect(() => {
Expand Down Expand Up @@ -131,6 +136,8 @@ export const GenerateLogModal: React.FC<Props> = ({ roomId, visible, onClose }:
messages: logData.data.result,
participants: participantsRef.current,
filter: ChannelsFilterOptions.toFilter(channelsFilterOptionsRef.current),
showCreatedAt: showCreatedAtRef.current,
showUsernameAlways: showUsernameAlwaysRef.current,
}),
`log_simple_${moment(new Date()).format('YYYY-MM-DD-HH-mm-ss')}.html`
);
Expand Down Expand Up @@ -177,6 +184,8 @@ export const GenerateLogModal: React.FC<Props> = ({ roomId, visible, onClose }:
channelsFilterOptionsRef,
configRef,
firebaseStorageRef,
showCreatedAtRef,
showUsernameAlwaysRef,
]);

return (
Expand Down Expand Up @@ -214,6 +223,28 @@ export const GenerateLogModal: React.FC<Props> = ({ roomId, visible, onClose }:
onChange={setChannelsFilterOptions}
disabled={isDownloading}
/>
{logMode === simple && (
<>
<div style={{ marginTop: 4 }}>ログに含める情報</div>
<div>
<Checkbox
checked={showUsernameAlways}
disabled={isDownloading}
onChange={e => setShowUsernameAlways(e.target.checked)}
>
常にユーザー名を含める
</Checkbox>
<br />
<Checkbox
checked={showCreatedAt}
disabled={isDownloading}
onChange={e => setShowCreatedAt(e.target.checked)}
>
書き込み日時
</Checkbox>
</div>
</>
)}
<Button
style={{ alignSelf: 'start', marginTop: 8 }}
type='primary'
Expand Down
9 changes: 9 additions & 0 deletions apps/web-server/src/utils/message/generateHtml.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { div, generateHtml, HtmlObject } from './generateHtml';

describe('generateHtml', () => {
it('tests XSS', () => {
const htmlObject: HtmlObject = { type: div, children: '<script>alert("!")</script>' };
const htmlString = generateHtml(htmlObject);
expect(htmlString).not.toContain('<script>');
});
});
32 changes: 32 additions & 0 deletions apps/web-server/src/utils/message/generateHtml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { escape } from 'html-escaper';

export const div = 'div';
export const span = 'span';
type Type = typeof div | typeof span;

export type HtmlObject = {
type: Type;
className?: string;
style?: string;
children?: string | HtmlObject[];
};

export const generateHtml = ({ type, className, style, children }: HtmlObject) => {
let childrenAsHtml: string;
if (children == null || typeof children === 'string') {
childrenAsHtml = children == null ? '' : escape(children);
} else {
childrenAsHtml = '';
children.forEach(c => {
childrenAsHtml = `${childrenAsHtml}${generateHtml(c)}`;
});
}
let startTag = type;
if (className != null) {
startTag += ` class="${className}"`;
}
if (style != null) {
startTag += ` style="${style}"`;
}
return `<${startTag}>${childrenAsHtml}</${type}>`;
};
87 changes: 62 additions & 25 deletions apps/web-server/src/utils/message/roomLogGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { logHtml } from './richLogHtml';
import { RoomMessageFilter } from '../../components/contextual/room/message/ChannelsFilter';
import { WebConfig } from '../../configType';
import { FirebaseStorage as FirebaseStorageType } from '@firebase/storage';
import { div, generateHtml, HtmlObject, span } from './generateHtml';

const privateMessage = 'privateMessage';
const publicMessage = 'publicMessage';
Expand Down Expand Up @@ -124,7 +125,7 @@ const createRoomMessageArray = (
const privateChannelSet = new PrivateChannelSet(new Set(msg.visibleTo));
const channelName = privateChannelSet
.toChannelNameBase(participants)
.reduce((seed, elem, i) => (i === 0 ? elem : `${seed}, ${elem}`), '');
.reduce((seed, elem, i) => (i === 0 ? `秘話: ${elem}` : `${seed}, ${elem}`), '');
if (isDeleted(msg)) {
if (msg.createdBy == null) {
return;
Expand Down Expand Up @@ -237,34 +238,70 @@ type GenerateLogParams = {
filter: RoomMessageFilter;
} & PublicChannelNames;

export const generateAsStaticHtml = (params: GenerateLogParams) => {
export const generateAsStaticHtml = (
params: GenerateLogParams & {
showCreatedAt: boolean;
showUsernameAlways: boolean;
}
) => {
const elements = createRoomMessageArray(params)
.sort((x, y) => x.createdAt - y.createdAt)
.map(msg => {
const left =
msg.value.createdBy == null
? '<span>システムメッセージ</span>'
: `<span>${escape(msg.value.createdBy.rolePlayPart ?? '')}</span>
${msg.value.createdBy.rolePlayPart == null ? '' : '<span> - </span>'}
<span>${escape(msg.value.createdBy.participantNamePart)}</span>
<span> (${escape(msg.value.channelName)})</span>
<span> </span>`;

return `<div class="message" style="${
msg.value.textColor == null ? '' : `color: ${escape(msg.value.textColor)}`
}">
${left}
<span> @ ${moment(new Date(msg.createdAt)).format('MM/DD HH:mm:ss')} </span>
${
msg.value.text == null
? '<span class="text gray">(削除済み)</span>'
: `<span class="text">${escape(msg.value.text ?? '')} ${escape(
msg.value.commandResult ?? ''
)}</span>`
}
</div>`;
const left: HtmlObject[] = [];

if (msg.value.createdBy == null) {
left.push({
type: span,
children: 'システムメッセージ',
});
} else {
let name: HtmlObject;
if (msg.value.createdBy.rolePlayPart == null) {
name = { type: span, children: msg.value.createdBy.participantNamePart };
} else {
name = {
type: span,
children: params.showUsernameAlways
? `${msg.value.createdBy.rolePlayPart} - ${msg.value.createdBy.participantNamePart}`
: msg.value.createdBy.participantNamePart,
};
}
left.push(name);
const channel: HtmlObject = {
type: span,
children: ` (${msg.value.channelName})`,
};
left.push(channel);
}

const result: HtmlObject = {
type: div,
className: 'message',
style:
msg.value.textColor == null
? undefined
: `color: ${escape(msg.value.textColor)}`,
children: [
...left,
{
type: span,
children: params.showCreatedAt
? ` @ ${moment(new Date(msg.createdAt)).format('MM/DD HH:mm:ss')} `
: ' ',
},
{
type: span,
className: msg.value.text == null ? 'text gray' : 'text',
children:
msg.value.text == null
? '(削除済み)'
: `${msg.value.text ?? ''} ${msg.value.commandResult ?? ''}`,
},
],
};
return generateHtml(result);
})
.reduce((seed, elem) => seed + '\r\n' + elem, '');
.reduce((seed, elem) => (seed === '' ? elem : `${seed}\r\n${elem}`), '');

return `<!DOCTYPE html>
<html lang="ja">
Expand Down

0 comments on commit 0822d01

Please sign in to comment.