• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NodeBB / NodeBB / 23305017021

19 Mar 2026 04:20PM UTC coverage: 85.425% (-0.1%) from 85.545%
23305017021

push

github

nodebb-misty
chore: update changelog for v4.10.0

13445 of 18442 branches covered (72.9%)

28384 of 33227 relevant lines covered (85.42%)

3333.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.56
/src/posts/parse.js
1
'use strict';
2

3
const nconf = require('nconf');
32✔
4
const winston = require('winston');
32✔
5
const sanitize = require('sanitize-html');
32✔
6
const _ = require('lodash');
32✔
7

8
const meta = require('../meta');
32✔
9
const plugins = require('../plugins');
32✔
10
const translator = require('../translator');
32✔
11
const utils = require('../utils');
32✔
12
const postCache = require('./cache');
32✔
13
const devMode = process.env.NODE_ENV === 'development';
32✔
14

15
let sanitizeConfig = {
32✔
16
        allowedTags: sanitize.defaults.allowedTags.concat([
17
                // Some safe-to-use tags to add
18
                'ins', 'del', 'img', 'button',
19
                'video', 'audio', 'source', 'iframe', 'embed',
20
        ]),
21
        allowedAttributes: {
22
                ...sanitize.defaults.allowedAttributes,
23
                a: ['href', 'name', 'hreflang', 'media', 'rel', 'target', 'type'],
24
                img: ['alt', 'height', 'ismap', 'src', 'usemap', 'width', 'srcset'],
25
                iframe: ['height', 'name', 'src', 'width', 'allow', 'frameborder'],
26
                video: ['autoplay', 'playsinline', 'controls', 'height', 'loop', 'muted', 'poster', 'preload', 'src', 'width'],
27
                audio: ['autoplay', 'controls', 'loop', 'muted', 'preload', 'src'],
28
                source: ['type', 'src', 'srcset', 'sizes', 'media', 'height', 'width'],
29
                embed: ['height', 'src', 'type', 'width'],
30
        },
31
        nonBooleanAttributes: ['accesskey', 'class', 'contenteditable', 'dir',
32
                'draggable', 'dropzone', 'hidden', 'id', 'lang', 'spellcheck', 'style',
33
                'tabindex', 'title', 'translate', 'aria-*', 'data-*',
34
        ],
35
};
36
const allowedTypes = new Set(['default', 'plaintext', 'activitypub.note', 'activitypub.article', 'markdown']);
32✔
37

38
module.exports = function (Posts) {
32✔
39
        Posts.urlRegex = /href="([^"]+)"/g;
32✔
40
        Posts.imgRegex = /src="([^"]+)"/g;
32✔
41
        Posts.mdImageUrlRegex = /\[.+?\]\(([^\\)]+)\)/g;
32✔
42

43
        Posts.parsePost = async function (postData, type) {
32✔
44
                if (!postData) {
5,370✔
45
                        return postData;
4✔
46
                }
47

48
                if (!type || !allowedTypes.has(type)) {
5,366✔
49
                        type = 'default';
3,209✔
50
                }
51
                postData.content = String(postData.sourceContent || postData.content || '');
5,366✔
52
                const cache = postCache.getOrCreate();
5,366✔
53
                const cacheKey = `${String(postData.pid)}|${type}`;
5,366✔
54
                const cachedContent = cache.get(cacheKey);
5,366✔
55

56
                if (postData.pid && cachedContent !== undefined) {
5,366✔
57
                        postData.content = cachedContent;
1,726✔
58
                        return postData;
1,726✔
59
                }
60

61
                if (!type.startsWith('activitypub.')) {
3,640✔
62
                        postData.content = postData.content.replace(meta.config.activitypubBreakString, '');
3,332✔
63
                }
64
                ({ postData } = await plugins.hooks.fire('filter:parse.post', { postData, type }));
3,640✔
65
                postData.content = translator.escape(postData.content);
3,640✔
66
                if (postData.pid) {
3,640✔
67
                        cache.set(cacheKey, postData.content);
3,492✔
68
                }
69

70
                return postData;
3,640✔
71
        };
72

73
        Posts.clearCachedPost = function (pid) {
32✔
74
                const cache = require('./cache');
172✔
75
                cache.del(Array.from(allowedTypes).map(type => `${String(pid)}|${type}`));
860✔
76
        };
77

78
        Posts.parseSignature = async function (userData, uid) {
32✔
79
                userData.signature = sanitizeSignature(userData.signature || '');
196✔
80
                return await plugins.hooks.fire('filter:parse.signature', { userData: userData, uid: uid });
196✔
81
        };
82

83
        Posts.relativeToAbsolute = function (content, regex) {
32✔
84
                // Turns relative links in content to absolute urls
85
                if (!content) {
681✔
86
                        return content;
117✔
87
                }
88
                let parsed;
89
                let current = regex.exec(content);
564✔
90
                let absolute;
91
                while (current !== null) {
564✔
92
                        if (current[1]) {
20!
93
                                try {
20✔
94
                                        parsed = new URL(current[1], nconf.get('url'));
20✔
95
                                        absolute = parsed.toString();
20✔
96
                                        if (absolute !== current[1]) {
20!
97
                                                const offset = current[0].indexOf(current[1]);
20✔
98
                                                content = content.slice(0, current.index + offset) +
20✔
99
                                                absolute +
100
                                                content.slice(current.index + offset + current[1].length);
101
                                        }
102
                                } catch (err) {
103
                                        if (devMode) {
×
104
                                                winston.verbose(err.messsage);
×
105
                                        }
106
                                }
107
                        }
108
                        current = regex.exec(content);
20✔
109
                }
110

111
                return content;
564✔
112
        };
113

114
        Posts.sanitize = function (content) {
32✔
115
                return sanitize(content, {
3,849✔
116
                        allowedTags: sanitizeConfig.allowedTags,
117
                        allowedAttributes: sanitizeConfig.allowedAttributes,
118
                        allowedClasses: sanitizeConfig.allowedClasses,
119
                });
120
        };
121

122
        Posts.sanitizePlaintext = content => sanitize(content, {
1,543✔
123
                allowedTags: [],
124
        });
125

126
        Posts.configureSanitize = async () => {
32✔
127
                // Each allowed tags should have some common global attributes...
128
                sanitizeConfig.allowedTags.forEach((tag) => {
4✔
129
                        sanitizeConfig.allowedAttributes[tag] = _.union(
316✔
130
                                sanitizeConfig.allowedAttributes[tag],
131
                                sanitizeConfig.nonBooleanAttributes
132
                        );
133
                });
134

135
                // Some plugins might need to adjust or whitelist their own tags...
136
                sanitizeConfig = await plugins.hooks.fire('filter:sanitize.config', sanitizeConfig);
4✔
137
        };
138

139
        Posts.registerHooks = () => {
32✔
140
                plugins.hooks.register('core', {
4✔
141
                        hook: 'filter:parse.post',
142
                        method: async (data) => {
143
                                data.postData.content = Posts[data.type !== 'plaintext' ? 'sanitize' : 'sanitizePlaintext'](data.postData.content);
3,700✔
144
                                return data;
3,700✔
145
                        },
146
                });
147

148
                plugins.hooks.register('core', {
4✔
149
                        hook: 'filter:parse.raw',
150
                        method: async content => Posts.sanitize(content),
892✔
151
                });
152

153
                plugins.hooks.register('core', {
4✔
154
                        hook: 'filter:parse.aboutme',
155
                        method: async content => Posts.sanitize(content),
48✔
156
                });
157

158
                plugins.hooks.register('core', {
4✔
159
                        hook: 'filter:parse.signature',
160
                        method: async (data) => {
161
                                data.userData.signature = Posts.sanitize(data.userData.signature);
196✔
162
                                return data;
196✔
163
                        },
164
                });
165
        };
166

167
        function sanitizeSignature(signature) {
168
                signature = translator.escape(signature);
196✔
169
                const tagsToStrip = [];
196✔
170

171
                if (meta.config['signatures:disableLinks']) {
196✔
172
                        tagsToStrip.push('a');
4✔
173
                }
174

175
                if (meta.config['signatures:disableImages']) {
196✔
176
                        tagsToStrip.push('img');
4✔
177
                }
178

179
                return utils.stripHTMLTags(signature, tagsToStrip);
196✔
180
        }
181
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc