]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14670 Wrap calls to dompurify
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Fri, 23 Apr 2021 13:55:40 +0000 (15:55 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 27 Apr 2021 20:03:41 +0000 (20:03 +0000)
20 files changed:
server/sonar-web/src/main/js/apps/about/components/AboutApp.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/ActivationFormModal-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/CustomRuleFormModal-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/settings/components/Definition.tsx
server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
server/sonar-web/src/main/js/apps/settings/utils.ts
server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx
server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.tsx
server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/sanitize.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/testMocks.ts

index b7d899a1f0fd7947ffc43b5d6a42ef4ada6827b3..652f712e68900ba955d705d346eb4ad249d1d47a 100644 (file)
@@ -17,7 +17,6 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { sanitize } from 'dompurify';
 import { Location } from 'history';
 import { keyBy } from 'lodash';
 import * as React from 'react';
@@ -31,6 +30,7 @@ import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget';
 import withIndexationContext, {
   WithIndexationContextProps
 } from '../../../components/hoc/withIndexationContext';
+import { sanitizeString } from '../../../helpers/sanitize';
 import {
   getAppState,
   getCurrentUser,
@@ -163,7 +163,7 @@ export class AboutApp extends React.PureComponent<Props, State> {
           <div
             className="about-page-section"
             // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: sanitize(customText) }}
+            dangerouslySetInnerHTML={{ __html: sanitizeString(customText) }}
           />
         )}
 
index 51470ba11e3a26b2deff8e30e9c5d5f15b87ef54..65d00e2a7c131a44e0f72d9c8d1addc17db5d198 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as classNames from 'classnames';
-import { sanitize } from 'dompurify';
 import * as React from 'react';
 import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
 import Modal from 'sonar-ui-common/components/controls/Modal';
@@ -28,6 +27,7 @@ import { translate } from 'sonar-ui-common/helpers/l10n';
 import { activateRule, Profile } from '../../../api/quality-profiles';
 import SeverityHelper from '../../../components/shared/SeverityHelper';
 import { SEVERITIES } from '../../../helpers/constants';
+import { sanitizeString } from '../../../helpers/sanitize';
 import { sortProfiles } from '../../quality-profiles/utils';
 
 interface Props {
@@ -222,11 +222,13 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat
                       value={this.state.params[param.key] || ''}
                     />
                   )}
-                  <div
-                    className="note"
-                    // eslint-disable-next-line react/no-danger
-                    dangerouslySetInnerHTML={{ __html: sanitize(param.htmlDesc || '') }}
-                  />
+                  {param.htmlDesc !== undefined && (
+                    <div
+                      className="note"
+                      // eslint-disable-next-line react/no-danger
+                      dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
+                    />
+                  )}
                 </div>
               ))
             )}
index 738187b2be771c2b484e3d8b26774f3964805fc7..d21c7e8349828687c476e36011f0167dc3a6b441 100644 (file)
@@ -17,7 +17,6 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { sanitize } from 'dompurify';
 import * as React from 'react';
 import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
 import Modal from 'sonar-ui-common/components/controls/Modal';
@@ -33,6 +32,7 @@ import FormattingTips from '../../../components/common/FormattingTips';
 import SeverityHelper from '../../../components/shared/SeverityHelper';
 import TypeHelper from '../../../components/shared/TypeHelper';
 import { RULE_STATUSES, RULE_TYPES, SEVERITIES } from '../../../helpers/constants';
+import { sanitizeString } from '../../../helpers/sanitize';
 
 interface Props {
   customRule?: T.RuleDetails;
@@ -304,11 +304,13 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
           value={this.state.params[param.key] || ''}
         />
       )}
-      <div
-        className="modal-field-description"
-        // eslint-disable-next-line react/no-danger
-        dangerouslySetInnerHTML={{ __html: sanitize(param.htmlDesc || '') }}
-      />
+      {param.htmlDesc !== undefined && (
+        <div
+          className="modal-field-description"
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
+        />
+      )}
     </div>
   );
 
index 8e1219a8d84e2e808bbbe193f66087c80f666e1c..d89059fa468e60a19e31a31ba73bccfb47d9a62e 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { sanitize } from 'dompurify';
 import * as React from 'react';
 import { Button, ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
 import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
 import { updateRule } from '../../../api/rules';
 import FormattingTips from '../../../components/common/FormattingTips';
+import { sanitizeString } from '../../../helpers/sanitize';
 import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal';
 
 interface Props {
@@ -112,7 +112,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
         <div
           className="rule-desc spacer-bottom markdown"
           // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: sanitize(this.props.ruleDetails.htmlNote) }}
+          dangerouslySetInnerHTML={{ __html: sanitizeString(this.props.ruleDetails.htmlNote) }}
         />
       )}
       {this.props.canWrite && (
@@ -190,11 +190,11 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
 
     return (
       <div className="js-rule-description">
-        {hasDescription ? (
+        {hasDescription && ruleDetails.htmlDesc !== undefined ? (
           <div
             className="coding-rules-detail-description rule-desc markdown"
             // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: sanitize(ruleDetails.htmlDesc || '') }}
+            dangerouslySetInnerHTML={{ __html: sanitizeString(ruleDetails.htmlDesc) }}
           />
         ) : (
           <div className="coding-rules-detail-description rule-desc markdown">
index c134954aa0514a3a284eacb331e6170e6a1ec019..91cf2b95cd451f13fbe86d16a5d7876a7f502d39 100644 (file)
@@ -17,9 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { sanitize } from 'dompurify';
 import * as React from 'react';
 import { translate } from 'sonar-ui-common/helpers/l10n';
+import { sanitizeString } from '../../../helpers/sanitize';
 
 interface Props {
   params: T.RuleParameter[];
@@ -30,10 +30,12 @@ export default class RuleDetailsParameters extends React.PureComponent<Props> {
     <tr className="coding-rules-detail-parameter" key={param.key}>
       <td className="coding-rules-detail-parameter-name">{param.key}</td>
       <td className="coding-rules-detail-parameter-description">
-        <p
-          // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: sanitize(param.htmlDesc || '') }}
-        />
+        {param.htmlDesc !== undefined && (
+          <p
+            // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
+          />
+        )}
         {param.defaultValue !== undefined && (
           <div className="note spacer-top">
             {translate('coding_rules.parameters.default_value')}
index e011e79d46f07a3b4dd9211c55f1ab5466292bf3..2f28c908ec2ef9b9be9a71991791bfc6c3f9f1c8 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { mockQualityProfile, mockRule } from '../../../../helpers/testMocks';
+import {
+  mockQualityProfile,
+  mockRule,
+  mockRuleActivation,
+  mockRuleDetails,
+  mockRuleDetailsParameter
+} from '../../../../helpers/testMocks';
 import ActivationFormModal from '../ActivationFormModal';
 
-it('render correctly', () => {
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
   expect(
-    shallow(
-      <ActivationFormModal
-        modalHeader="title"
-        onClose={jest.fn()}
-        onDone={jest.fn()}
-        profiles={[mockQualityProfile()]}
-        rule={mockRule()}
-      />
-    )
-  ).toMatchSnapshot();
+    shallowRender({
+      profiles: [
+        mockQualityProfile(),
+        mockQualityProfile({ depth: 2, actions: { edit: true }, language: 'js' })
+      ]
+    })
+  ).toMatchSnapshot('with deep profiles');
+  expect(shallowRender({ rule: mockRuleDetails({ templateKey: 'foobar' }) })).toMatchSnapshot(
+    'custom rule'
+  );
+  expect(shallowRender({ activation: mockRuleActivation() })).toMatchSnapshot('update mode');
+  const wrapper = shallowRender();
+  wrapper.setState({ submitting: true });
+  expect(wrapper).toMatchSnapshot('submitting');
 });
+
+function shallowRender(props: Partial<ActivationFormModal['props']> = {}) {
+  return shallow<ActivationFormModal>(
+    <ActivationFormModal
+      modalHeader="title"
+      onClose={jest.fn()}
+      onDone={jest.fn()}
+      profiles={[mockQualityProfile()]}
+      rule={mockRule({
+        params: [
+          mockRuleDetailsParameter(),
+          mockRuleDetailsParameter({ key: '2', type: 'TEXT', htmlDesc: undefined })
+        ]
+      })}
+      {...props}
+    />
+  );
+}
index eb49f339c08bb040836d6f8b7de3ed69b8ad9563..895e063555eff9d3f8e5720fcfee00111b5ca13c 100644 (file)
@@ -21,13 +21,13 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import { submit, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
 import { createRule } from '../../../../api/rules';
-import { mockRule } from '../../../../helpers/testMocks';
+import { mockRule, mockRuleDetailsParameter } from '../../../../helpers/testMocks';
 import CustomRuleFormModal from '../CustomRuleFormModal';
 
 jest.mock('../../../../api/rules', () => ({ createRule: jest.fn() }));
 
 it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot();
+  expect(shallowRender()).toMatchSnapshot('default');
 });
 
 it('should handle re-activation', async () => {
@@ -43,7 +43,16 @@ function shallowRender(props: Partial<CustomRuleFormModal['props']> = {}) {
     <CustomRuleFormModal
       onClose={jest.fn()}
       onDone={jest.fn()}
-      templateRule={{ ...mockRule(), createdAt: 'date', repo: 'squid' }}
+      templateRule={{
+        ...mockRule({
+          params: [
+            mockRuleDetailsParameter(),
+            mockRuleDetailsParameter({ key: '2', type: 'TEXT', htmlDesc: undefined })
+          ]
+        }),
+        createdAt: 'date',
+        repo: 'squid'
+      }}
       {...props}
     />
   );
index 2225b883507423fac7c8be09be441765ac8abf3f..2adb2cf8b78995a8c327a606ce74d1c21d77c266 100644 (file)
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`render correctly 1`] = `
+exports[`should render correctly: custom rule 1`] = `
 <Modal
   contentLabel="title"
   onRequestClose={[MockFunction]}
@@ -80,12 +80,597 @@ exports[`render correctly 1`] = `
           valueRenderer={[Function]}
         />
       </div>
+      <div
+        className="modal-field"
+      >
+        <p
+          className="note"
+        >
+          coding_rules.custom_rule.activation_notice
+        </p>
+      </div>
+    </div>
+    <footer
+      className="modal-foot"
+    >
+      <SubmitButton
+        disabled={true}
+      >
+        coding_rules.activate
+      </SubmitButton>
+      <ResetButtonLink
+        disabled={false}
+        onClick={[MockFunction]}
+      >
+        cancel
+      </ResetButtonLink>
+    </footer>
+  </form>
+</Modal>
+`;
+
+exports[`should render correctly: default 1`] = `
+<Modal
+  contentLabel="title"
+  onRequestClose={[MockFunction]}
+  size="small"
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <div
+      className="modal-head"
+    >
+      <h2>
+        title
+      </h2>
+    </div>
+    <div
+      className="modal-body modal-container"
+    >
+      <Alert
+        variant="info"
+      >
+        coding_rules.active_in_all_profiles
+      </Alert>
+      <div
+        className="modal-field"
+      >
+        <label>
+          coding_rules.quality_profile
+        </label>
+        <Select
+          className="js-profile"
+          clearable={false}
+          disabled={false}
+          onChange={[Function]}
+          options={Array []}
+          value=""
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label>
+          severity
+        </label>
+        <Select
+          className="js-severity"
+          clearable={false}
+          disabled={false}
+          onChange={[Function]}
+          optionRenderer={[Function]}
+          options={
+            Array [
+              Object {
+                "label": "severity.BLOCKER",
+                "value": "BLOCKER",
+              },
+              Object {
+                "label": "severity.CRITICAL",
+                "value": "CRITICAL",
+              },
+              Object {
+                "label": "severity.MAJOR",
+                "value": "MAJOR",
+              },
+              Object {
+                "label": "severity.MINOR",
+                "value": "MINOR",
+              },
+              Object {
+                "label": "severity.INFO",
+                "value": "INFO",
+              },
+            ]
+          }
+          searchable={false}
+          value="MAJOR"
+          valueRenderer={[Function]}
+        />
+      </div>
+      <div
+        className="modal-field"
+        key="1"
+      >
+        <label
+          title="1"
+        >
+          1
+        </label>
+        <input
+          disabled={false}
+          name="1"
+          onChange={[Function]}
+          placeholder="1"
+          type="text"
+          value="1"
+        />
+        <div
+          className="note"
+          dangerouslySetInnerHTML={
+            Object {
+              "__html": "description",
+            }
+          }
+        />
+      </div>
+      <div
+        className="modal-field"
+        key="2"
+      >
+        <label
+          title="2"
+        >
+          2
+        </label>
+        <textarea
+          disabled={false}
+          name="2"
+          onChange={[Function]}
+          placeholder="1"
+          rows={3}
+          value="1"
+        />
+      </div>
+    </div>
+    <footer
+      className="modal-foot"
+    >
+      <SubmitButton
+        disabled={true}
+      >
+        coding_rules.activate
+      </SubmitButton>
+      <ResetButtonLink
+        disabled={false}
+        onClick={[MockFunction]}
+      >
+        cancel
+      </ResetButtonLink>
+    </footer>
+  </form>
+</Modal>
+`;
+
+exports[`should render correctly: submitting 1`] = `
+<Modal
+  contentLabel="title"
+  onRequestClose={[MockFunction]}
+  size="small"
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <div
+      className="modal-head"
+    >
+      <h2>
+        title
+      </h2>
+    </div>
+    <div
+      className="modal-body modal-container"
+    >
+      <Alert
+        variant="info"
+      >
+        coding_rules.active_in_all_profiles
+      </Alert>
+      <div
+        className="modal-field"
+      >
+        <label>
+          coding_rules.quality_profile
+        </label>
+        <Select
+          className="js-profile"
+          clearable={false}
+          disabled={true}
+          onChange={[Function]}
+          options={Array []}
+          value=""
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label>
+          severity
+        </label>
+        <Select
+          className="js-severity"
+          clearable={false}
+          disabled={true}
+          onChange={[Function]}
+          optionRenderer={[Function]}
+          options={
+            Array [
+              Object {
+                "label": "severity.BLOCKER",
+                "value": "BLOCKER",
+              },
+              Object {
+                "label": "severity.CRITICAL",
+                "value": "CRITICAL",
+              },
+              Object {
+                "label": "severity.MAJOR",
+                "value": "MAJOR",
+              },
+              Object {
+                "label": "severity.MINOR",
+                "value": "MINOR",
+              },
+              Object {
+                "label": "severity.INFO",
+                "value": "INFO",
+              },
+            ]
+          }
+          searchable={false}
+          value="MAJOR"
+          valueRenderer={[Function]}
+        />
+      </div>
+      <div
+        className="modal-field"
+        key="1"
+      >
+        <label
+          title="1"
+        >
+          1
+        </label>
+        <input
+          disabled={true}
+          name="1"
+          onChange={[Function]}
+          placeholder="1"
+          type="text"
+          value="1"
+        />
+        <div
+          className="note"
+          dangerouslySetInnerHTML={
+            Object {
+              "__html": "description",
+            }
+          }
+        />
+      </div>
+      <div
+        className="modal-field"
+        key="2"
+      >
+        <label
+          title="2"
+        >
+          2
+        </label>
+        <textarea
+          disabled={true}
+          name="2"
+          onChange={[Function]}
+          placeholder="1"
+          rows={3}
+          value="1"
+        />
+      </div>
+    </div>
+    <footer
+      className="modal-foot"
+    >
+      <i
+        className="spinner spacer-right"
+      />
+      <SubmitButton
+        disabled={true}
+      >
+        coding_rules.activate
+      </SubmitButton>
+      <ResetButtonLink
+        disabled={true}
+        onClick={[MockFunction]}
+      >
+        cancel
+      </ResetButtonLink>
+    </footer>
+  </form>
+</Modal>
+`;
+
+exports[`should render correctly: update mode 1`] = `
+<Modal
+  contentLabel="title"
+  onRequestClose={[MockFunction]}
+  size="small"
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <div
+      className="modal-head"
+    >
+      <h2>
+        title
+      </h2>
+    </div>
+    <div
+      className="modal-body modal-container"
+    >
+      <div
+        className="modal-field"
+      >
+        <label>
+          coding_rules.quality_profile
+        </label>
+        <Select
+          className="js-profile"
+          clearable={false}
+          disabled={false}
+          onChange={[Function]}
+          options={Array []}
+          value=""
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label>
+          severity
+        </label>
+        <Select
+          className="js-severity"
+          clearable={false}
+          disabled={false}
+          onChange={[Function]}
+          optionRenderer={[Function]}
+          options={
+            Array [
+              Object {
+                "label": "severity.BLOCKER",
+                "value": "BLOCKER",
+              },
+              Object {
+                "label": "severity.CRITICAL",
+                "value": "CRITICAL",
+              },
+              Object {
+                "label": "severity.MAJOR",
+                "value": "MAJOR",
+              },
+              Object {
+                "label": "severity.MINOR",
+                "value": "MINOR",
+              },
+              Object {
+                "label": "severity.INFO",
+                "value": "INFO",
+              },
+            ]
+          }
+          searchable={false}
+          value="MAJOR"
+          valueRenderer={[Function]}
+        />
+      </div>
+      <div
+        className="modal-field"
+        key="1"
+      >
+        <label
+          title="1"
+        >
+          1
+        </label>
+        <input
+          disabled={false}
+          name="1"
+          onChange={[Function]}
+          placeholder="1"
+          type="text"
+          value="1"
+        />
+        <div
+          className="note"
+          dangerouslySetInnerHTML={
+            Object {
+              "__html": "description",
+            }
+          }
+        />
+      </div>
+      <div
+        className="modal-field"
+        key="2"
+      >
+        <label
+          title="2"
+        >
+          2
+        </label>
+        <textarea
+          disabled={false}
+          name="2"
+          onChange={[Function]}
+          placeholder="1"
+          rows={3}
+          value="1"
+        />
+      </div>
     </div>
     <footer
       className="modal-foot"
     >
       <SubmitButton
         disabled={true}
+      >
+        save
+      </SubmitButton>
+      <ResetButtonLink
+        disabled={false}
+        onClick={[MockFunction]}
+      >
+        cancel
+      </ResetButtonLink>
+    </footer>
+  </form>
+</Modal>
+`;
+
+exports[`should render correctly: with deep profiles 1`] = `
+<Modal
+  contentLabel="title"
+  onRequestClose={[MockFunction]}
+  size="small"
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <div
+      className="modal-head"
+    >
+      <h2>
+        title
+      </h2>
+    </div>
+    <div
+      className="modal-body modal-container"
+    >
+      <div
+        className="modal-field"
+      >
+        <label>
+          coding_rules.quality_profile
+        </label>
+        <Select
+          className="js-profile"
+          clearable={false}
+          disabled={true}
+          onChange={[Function]}
+          options={
+            Array [
+              Object {
+                "label": "name",
+                "value": "key",
+              },
+            ]
+          }
+          value="key"
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label>
+          severity
+        </label>
+        <Select
+          className="js-severity"
+          clearable={false}
+          disabled={false}
+          onChange={[Function]}
+          optionRenderer={[Function]}
+          options={
+            Array [
+              Object {
+                "label": "severity.BLOCKER",
+                "value": "BLOCKER",
+              },
+              Object {
+                "label": "severity.CRITICAL",
+                "value": "CRITICAL",
+              },
+              Object {
+                "label": "severity.MAJOR",
+                "value": "MAJOR",
+              },
+              Object {
+                "label": "severity.MINOR",
+                "value": "MINOR",
+              },
+              Object {
+                "label": "severity.INFO",
+                "value": "INFO",
+              },
+            ]
+          }
+          searchable={false}
+          value="MAJOR"
+          valueRenderer={[Function]}
+        />
+      </div>
+      <div
+        className="modal-field"
+        key="1"
+      >
+        <label
+          title="1"
+        >
+          1
+        </label>
+        <input
+          disabled={false}
+          name="1"
+          onChange={[Function]}
+          placeholder="1"
+          type="text"
+          value="1"
+        />
+        <div
+          className="note"
+          dangerouslySetInnerHTML={
+            Object {
+              "__html": "description",
+            }
+          }
+        />
+      </div>
+      <div
+        className="modal-field"
+        key="2"
+      >
+        <label
+          title="2"
+        >
+          2
+        </label>
+        <textarea
+          disabled={false}
+          name="2"
+          onChange={[Function]}
+          placeholder="1"
+          rows={3}
+          value="1"
+        />
+      </div>
+    </div>
+    <footer
+      className="modal-foot"
+    >
+      <SubmitButton
+        disabled={false}
       >
         coding_rules.activate
       </SubmitButton>
index 426e1a46317dcca67c259dfe5b774d45f4032cd0..3aed94f5b25e572806541510bd4c6e374b9b6d0f 100644 (file)
@@ -205,6 +205,54 @@ exports[`should handle re-activation 1`] = `
           className="modal-field-descriptor text-right"
         />
       </div>
+      <div
+        className="modal-field"
+        key="1"
+      >
+        <label
+          className="capitalize"
+          htmlFor="1"
+        >
+          1
+        </label>
+        <input
+          disabled={false}
+          id="1"
+          name="1"
+          onChange={[Function]}
+          placeholder="1"
+          type="text"
+          value=""
+        />
+        <div
+          className="modal-field-description"
+          dangerouslySetInnerHTML={
+            Object {
+              "__html": "description",
+            }
+          }
+        />
+      </div>
+      <div
+        className="modal-field"
+        key="2"
+      >
+        <label
+          className="capitalize"
+          htmlFor="2"
+        >
+          2
+        </label>
+        <textarea
+          disabled={false}
+          id="2"
+          name="2"
+          onChange={[Function]}
+          placeholder="1"
+          rows={3}
+          value=""
+        />
+      </div>
     </div>
     <div
       className="modal-foot"
@@ -226,7 +274,7 @@ exports[`should handle re-activation 1`] = `
 </Modal>
 `;
 
-exports[`should render correctly 1`] = `
+exports[`should render correctly: default 1`] = `
 <Modal
   contentLabel="coding_rules.create_custom_rule"
   onRequestClose={[MockFunction]}
@@ -426,6 +474,54 @@ exports[`should render correctly 1`] = `
           className="modal-field-descriptor text-right"
         />
       </div>
+      <div
+        className="modal-field"
+        key="1"
+      >
+        <label
+          className="capitalize"
+          htmlFor="1"
+        >
+          1
+        </label>
+        <input
+          disabled={false}
+          id="1"
+          name="1"
+          onChange={[Function]}
+          placeholder="1"
+          type="text"
+          value=""
+        />
+        <div
+          className="modal-field-description"
+          dangerouslySetInnerHTML={
+            Object {
+              "__html": "description",
+            }
+          }
+        />
+      </div>
+      <div
+        className="modal-field"
+        key="2"
+      >
+        <label
+          className="capitalize"
+          htmlFor="2"
+        >
+          2
+        </label>
+        <textarea
+          disabled={false}
+          id="2"
+          name="2"
+          onChange={[Function]}
+          placeholder="1"
+          rows={3}
+          value=""
+        />
+      </div>
     </div>
     <div
       className="modal-foot"
index a7765f34e6b863c9bc34220adffb6fdc0f49fc5d..98d06cf4c95de50f28789efa52a25b5dd2380715 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as classNames from 'classnames';
-import { sanitize } from 'dompurify';
 import * as React from 'react';
 import { Button, DeleteButton, EditButton } from 'sonar-ui-common/components/controls/buttons';
 import Dropdown, { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
@@ -28,6 +27,7 @@ import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
 import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
 import IssueChangelogDiff from '../../../components/issue/components/IssueChangelogDiff';
 import Avatar from '../../../components/ui/Avatar';
+import { sanitizeString } from '../../../helpers/sanitize';
 import { Hotspot, ReviewHistoryType } from '../../../types/security-hotspots';
 import { getHotspotReviewHistory } from '../utils';
 import HotspotCommentPopup from './HotspotCommentPopup';
@@ -89,7 +89,11 @@ export default function HotspotReviewHistory(props: HotspotReviewHistoryProps) {
 
             {type === ReviewHistoryType.Comment && key && html && markdown && (
               <div className="spacer-top display-flex-space-between">
-                <div className="markdown" dangerouslySetInnerHTML={{ __html: sanitize(html) }} />
+                <div
+                  className="markdown"
+                  // eslint-disable-next-line react/no-danger
+                  dangerouslySetInnerHTML={{ __html: sanitizeString(html) }}
+                />
                 {updatable && (
                   <div>
                     <div className="dropdown">
index 3b91bfbd559b8df8f067ad633ae62291e1bb4795..8b736e83003ff69e85ca5cc6c495646e617a59b3 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { sanitize } from 'dompurify';
 import * as React from 'react';
 import BoxedTabs from 'sonar-ui-common/components/controls/BoxedTabs';
 import Tab from 'sonar-ui-common/components/controls/Tabs';
 import { translate } from 'sonar-ui-common/helpers/l10n';
+import { sanitizeString } from '../../../helpers/sanitize';
 import { Hotspot } from '../../../types/security-hotspots';
 
 interface Props {
@@ -104,7 +104,8 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
         <div className="bordered huge-spacer-bottom">
           <div
             className="markdown big-padded"
-            dangerouslySetInnerHTML={{ __html: sanitize(currentTab.content) }}
+            // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: sanitizeString(currentTab.content) }}
           />
         </div>
       </>
index d37c7753bc44fdf75b3556825f43491c87534c08..aa0c97c7898cf4b1ac65f2e60dc7590dc53093cd 100644 (file)
@@ -22,7 +22,7 @@ import {
   SettingCategoryDefinition,
   SettingFieldDefinition
 } from '../../../types/settings';
-import { getDefaultValue, getEmptyValue, sanitizeTranslation } from '../utils';
+import { getDefaultValue, getEmptyValue } from '../utils';
 
 const fields = [
   { key: 'foo', type: 'STRING' } as SettingFieldDefinition,
@@ -82,106 +82,3 @@ describe('#getDefaultValue()', () => {
     }
   );
 });
-
-describe('sanitizeTranslation', () => {
-  it('should preserve formatting tags', () => {
-    const allowed = `
-    Hi this is <i>in italics</i> and <ul>
-    <li> lists </li>
-    <li> are allowed</li>
-    </ul>
-    <p>
-    as well. This is <b>Amazing</b> and this <strong>bold</strong> <br>
-    and <code>code.is.accepted too</code>
-    </p>
-  `;
-
-    const clean = sanitizeTranslation(allowed);
-    expect(clean).toBe(allowed);
-  });
-
-  /*
-   * Test code borrowed from OWASP's sanitizer tests
-   * https://github.com/OWASP/java-html-sanitizer/blob/master/src/test/resources/org/owasp/html/htmllexerinput1.html
-   */
-  it('should strip everything else', () => {
-    const clean = sanitizeTranslation(`<?xml version="not-even-close"?>
-
-    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
-    
-    <!-- a test input for HtmlLexer -->
-    
-    <html>
-    <head>
-    <title>Test File For HtmlLexer &amp; HtmlParser</title>
-    <link rel=stylesheet type="text/css" src=foo/bar.css />
-    <body
-     bgcolor=white
-     linkcolor = "blue"
-     onload="document.writeln(
-      &quot;&lt;p&gt;properly escaped code in a handler&lt;/p&gt;&quot;);"
-    >
-    
-    <script type="text/javascript"><!--
-    document.writeln("<p>Some initialization code in global context</p>");
-    --></script>
-    
-    <script type="text/javascript">
-    // hi there
-    document.writeln("<p>More initialization</p>");
-    </script>
-    
-    <div id=clickydiv onclick="handleClicky(event)"
-     ondblclick=this.onclick(event);return(false)>
-    Clicky
-    </div>
-    
-    <input id=foo>
-    <gxp:attr name="onchange">alert("&lt;b&gt;hi&lt;/b&gt;");</gxp:attr>
-    </input>
-    
-    <pre>&lt;div id=notarealtag onclick=notcode()&gt;</pre>
-    
-    <!-- some tokenization corner cases -->
-    
-    < notatag <atag/>
-    
-    </ notatag> </redundantlyclosed/>
-    
-    <messyattributes a=b=c d="e"f=g h =i j= k l = m checked n="o"/>
-    
-    < < < all in one text block > > >
-    
-    <xmp>Make sure that <!-- comments don't obscure the xmp close</xmp>
-    <% # some php code here
-    write("<pre>$horriblySyntacticConstruct1</pre>\n\n");
-    %>
-    <script type="text/javascript"><!--
-    alert("hello world");
-    // --></script>
-    
-    <script>/* </script> */alert('hi');</script>
-    <script><!--/* </script> */alert('hi');--></script>
-    
-    <xmp style=color:blue><!--/* </xmp> */alert('hi');--></xmp>
-    
-    <style><!-- p { contentf: '</style>' } --></style>
-    <style>Foo<!-- > </style> --></style>
-    <textarea><!-- Zoicks </textarea>--></textarea>
-    <!-- An escaping text span start may share its U+002D HYPHEN-MINUS characters
-       - with its corresponding escaping text span end. -->
-    <script><!--></script>
-    <script><!---></script>
-    <script><!----></script>
-    </body>
-    </html>
-    <![CDATA[ No such thing as a CDATA> section in HTML ]]>
-    <script>a<b</script>
-    <img src=foo.gif /><a href=><a href=/>
-    <span title=malformed attribs' do=don't id=foo checked onclick="a<b">Bar</span>`);
-
-    expect(clean.replace(/\s+/g, '')).toBe(
-      `Clickyalert("&lt;b&gt;hi&lt;/b&gt;");&lt;divid=notarealtagonclick=notcode()&gt;&lt;notatag&lt;&lt;&lt;allinonetextblock&gt;&gt;&gt;&lt;%#somephpcodeherewrite("$horriblySyntacticConstruct1");%&gt;*/alert('hi');*/alert('hi');--&gt;*/alert('hi');--&gt;'}--&gt;--&gt;&lt;!--Zoicks--&gt;sectioninHTML]]&gt;Bar`
-    );
-  });
-});
index 66a0d15c5f99ac1bb570b1b1f44ac1194da1e130..fadd728a2cf43254bcdd56442117d74920f2b950 100644 (file)
@@ -23,6 +23,7 @@ import { connect } from 'react-redux';
 import AlertErrorIcon from 'sonar-ui-common/components/icons/AlertErrorIcon';
 import AlertSuccessIcon from 'sonar-ui-common/components/icons/AlertSuccessIcon';
 import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { sanitizeStringRestricted } from '../../../helpers/sanitize';
 import {
   getSettingsAppChangedValue,
   getSettingsAppValidationMessage,
@@ -36,8 +37,7 @@ import {
   getPropertyDescription,
   getPropertyName,
   getSettingValue,
-  isDefaultOrInherited,
-  sanitizeTranslation
+  isDefaultOrInherited
 } from '../utils';
 import DefinitionActions from './DefinitionActions';
 import Input from './inputs/Input';
@@ -154,7 +154,8 @@ export class Definition extends React.PureComponent<Props, State> {
           {description && (
             <div
               className="markdown small spacer-top"
-              dangerouslySetInnerHTML={{ __html: sanitizeTranslation(description) }}
+              // eslint-disable-next-line react/no-danger
+              dangerouslySetInnerHTML={{ __html: sanitizeStringRestricted(description) }}
             />
           )}
 
index 22493ed1046cd9dd6722d040c4e21775164fc481..62e9034f9fc6a3da9ba3d0799f23f20617151d20 100644 (file)
@@ -19,8 +19,9 @@
  */
 import { groupBy, isEqual, sortBy } from 'lodash';
 import * as React from 'react';
+import { sanitizeStringRestricted } from '../../../helpers/sanitize';
 import { Setting, SettingCategoryDefinition } from '../../../types/settings';
-import { getSubCategoryDescription, getSubCategoryName, sanitizeTranslation } from '../utils';
+import { getSubCategoryDescription, getSubCategoryName } from '../utils';
 import DefinitionsList from './DefinitionsList';
 import EmailForm from './EmailForm';
 
@@ -79,7 +80,10 @@ export default class SubCategoryDefinitionsList extends React.PureComponent<Prop
             {subCategory.description != null && (
               <div
                 className="settings-sub-category-description markdown"
-                dangerouslySetInnerHTML={{ __html: sanitizeTranslation(subCategory.description) }}
+                // eslint-disable-next-line react/no-danger
+                dangerouslySetInnerHTML={{
+                  __html: sanitizeStringRestricted(subCategory.description)
+                }}
               />
             )}
             <DefinitionsList
index 0fa364c5c0f147bfed9d4c0c5b4c53378146d2b2..e695e2cfaa3f82c43e67dc58393bd4ca2d6b5999 100644 (file)
@@ -17,7 +17,6 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { sanitize } from 'dompurify';
 import { hasMessage, translate } from 'sonar-ui-common/helpers/l10n';
 import { Setting, SettingCategoryDefinition, SettingDefinition } from '../../types/settings';
 
@@ -37,12 +36,6 @@ export interface DefaultInputProps {
   value: any;
 }
 
-export function sanitizeTranslation(html: string) {
-  return sanitize(html, {
-    ALLOWED_TAGS: ['b', 'br', 'code', 'i', 'li', 'p', 'strong', 'ul']
-  });
-}
-
 export function getPropertyName(definition: SettingDefinition) {
   const key = `property.${definition.key}.name`;
   return hasMessage(key) ? translate(key) : definition.name;
index 4e599711a28cb44ab198075f72e28b00f4cf8900..dc99b50368fdc6504537ee09c8044beabd9dbd3f 100644 (file)
@@ -17,7 +17,6 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { sanitize } from 'dompurify';
 import * as React from 'react';
 import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
 import Modal from 'sonar-ui-common/components/controls/Modal';
@@ -25,6 +24,7 @@ import WarningIcon from 'sonar-ui-common/components/icons/WarningIcon';
 import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { dismissAnalysisWarning, getTask } from '../../api/ce';
+import { sanitizeStringRestricted } from '../../helpers/sanitize';
 import { TaskWarning } from '../../types/tasks';
 import { withCurrentUser } from '../hoc/withCurrentUser';
 
@@ -137,9 +137,7 @@ export class AnalysisWarningsModal extends React.PureComponent<Props, State> {
                   <span
                     // eslint-disable-next-line react/no-danger
                     dangerouslySetInnerHTML={{
-                      __html: sanitize(message.trim().replace(/\n/g, '<br />'), {
-                        ALLOWED_ATTR: ['target', 'href']
-                      })
+                      __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />'))
                     }}
                   />
 
index 84a617ccd88b5e7a4ef4e348d4d6517b4c19d412..06d4a937ed2211aa6aec6f67012a42ea82d278e2 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { sanitize } from 'dompurify';
 import * as React from 'react';
 import { DeleteButton, EditButton } from 'sonar-ui-common/components/controls/buttons';
 import Toggler from 'sonar-ui-common/components/controls/Toggler';
 import DateFromNow from 'sonar-ui-common/components/intl/DateFromNow';
 import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
 import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { sanitizeString } from '../../../helpers/sanitize';
 import Avatar from '../../ui/Avatar';
 import CommentDeletePopup from '../popups/CommentDeletePopup';
 import CommentPopup from '../popups/CommentPopup';
@@ -96,7 +96,8 @@ export default class IssueCommentLine extends React.PureComponent<Props, State>
         </div>
         <div
           className="issue-comment-text markdown"
-          dangerouslySetInnerHTML={{ __html: sanitize(comment.htmlText) }}
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{ __html: sanitizeString(comment.htmlText) }}
         />
         <div className="issue-comment-age">
           <span className="a11y-hidden">{translate('issue.comment.posted_on')}</span>
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts
new file mode 100644 (file)
index 0000000..1d90667
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { sanitizeString, sanitizeStringRestricted } from '../sanitize';
+
+describe('sanitizeStringRestricted', () => {
+  it('should preserve only specific formatting tags', () => {
+    expect(
+      sanitizeStringRestricted(`
+    Hi <a href="http://example.com" target="_blank">this</a> is <i>in italics</i> and <ul>
+    <li> lists </li>
+    <li> are allowed</li>
+    </ul>
+    <p>
+    as well. This is <b>Amazing</b> and this <strong>bold</strong> <br>
+    and <code>code.is.accepted too</code>
+    </p>
+  `)
+    ).toBe(`
+    Hi <a target="_blank" href="http://example.com">this</a> is <i>in italics</i> and <ul>
+    <li> lists </li>
+    <li> are allowed</li>
+    </ul>
+    <p>
+    as well. This is <b>Amazing</b> and this <strong>bold</strong> <br>
+    and <code>code.is.accepted too</code>
+    </p>
+  `);
+  });
+
+  /*
+   * Test code borrowed from OWASP's sanitizer tests
+   * https://github.com/OWASP/java-html-sanitizer/blob/master/src/test/resources/org/owasp/html/htmllexerinput1.html
+   */
+  it('should strip everything else', () => {
+    const clean = sanitizeStringRestricted(`<?xml version="not-even-close"?>
+
+    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+    
+    <!-- a test input for HtmlLexer -->
+    
+    <html>
+    <head>
+    <title>Test File For HtmlLexer &amp; HtmlParser</title>
+    <link rel=stylesheet type="text/css" src=foo/bar.css />
+    <body
+     bgcolor=white
+     linkcolor = "blue"
+     onload="document.writeln(
+      &quot;&lt;p&gt;properly escaped code in a handler&lt;/p&gt;&quot;);"
+    >
+    
+    <script type="text/javascript"><!--
+    document.writeln("<p>Some initialization code in global context</p>");
+    --></script>
+    
+    <script type="text/javascript">
+    // hi there
+    document.writeln("<p>More initialization</p>");
+    </script>
+    
+    <div id=clickydiv onclick="handleClicky(event)"
+     ondblclick=this.onclick(event);return(false)>
+    Clicky
+    </div>
+    
+    <input id=foo>
+    <gxp:attr name="onchange">alert("&lt;b&gt;hi&lt;/b&gt;");</gxp:attr>
+    </input>
+    
+    <pre>&lt;div id=notarealtag onclick=notcode()&gt;</pre>
+    
+    <!-- some tokenization corner cases -->
+    
+    < notatag <atag/>
+    
+    </ notatag> </redundantlyclosed/>
+    
+    <messyattributes a=b=c d="e"f=g h =i j= k l = m checked n="o"/>
+    
+    < < < all in one text block > > >
+    
+    <xmp>Make sure that <!-- comments don't obscure the xmp close</xmp>
+    <% # some php code here
+    write("<pre>$horriblySyntacticConstruct1</pre>\n\n");
+    %>
+    <script type="text/javascript"><!--
+    alert("hello world");
+    // --></script>
+    
+    <script>/* </script> */alert('hi');</script>
+    <script><!--/* </script> */alert('hi');--></script>
+    
+    <xmp style=color:blue><!--/* </xmp> */alert('hi');--></xmp>
+    
+    <style><!-- p { contentf: '</style>' } --></style>
+    <style>Foo<!-- > </style> --></style>
+    <textarea><!-- Zoicks </textarea>--></textarea>
+    <!-- An escaping text span start may share its U+002D HYPHEN-MINUS characters
+       - with its corresponding escaping text span end. -->
+    <script><!--></script>
+    <script><!---></script>
+    <script><!----></script>
+    </body>
+    </html>
+    <![CDATA[ No such thing as a CDATA> section in HTML ]]>
+    <script>a<b</script>
+    <img src=foo.gif /><a href=><a href=/>
+    <span title=malformed attribs' do=don't id=foo checked onclick="a<b">Bar</span>`);
+
+    expect(clean.replace(/\s+/g, '')).toBe(
+      `Clickyalert("&lt;b&gt;hi&lt;/b&gt;");&lt;divid=notarealtagonclick=notcode()&gt;&lt;notatag&lt;&lt;&lt;allinonetextblock&gt;&gt;&gt;&lt;%#somephpcodeherewrite("$horriblySyntacticConstruct1");%&gt;*/alert('hi');*/alert('hi');--&gt;*/alert('hi');--&gt;'}--&gt;--&gt;&lt;!--Zoicks--&gt;sectioninHTML]]&gt;<ahref=""></a><ahref="/">Bar</a>`
+    );
+  });
+});
+
+describe('sanitizeString', () => {
+  it('should not allow MathML and SVG', () => {
+    const tainted = `
+    Hi <a href="javascript:alert('hello')" target="_blank">this</a> is <i>in italics</i> and <ul>
+    <li> lists </li>
+    <li> are allowed</li>
+    </ul>
+    <p class="some-class">
+    as well. This is <b>Amazing</b> and this <strong>bold</strong> <br>
+    and <code>code.is.accepted too</code>
+    </p>
+    <svg><text>SVG isn't allowed</text></svg>
+    <math xmlns="http://www.w3.org/1998/Math/MathML">
+      <infinity />
+    </math>`;
+    const clean = `
+    Hi <a>this</a> is <i>in italics</i> and <ul>
+    <li> lists </li>
+    <li> are allowed</li>
+    </ul>
+    <p class="some-class">
+    as well. This is <b>Amazing</b> and this <strong>bold</strong> <br>
+    and <code>code.is.accepted too</code>
+    </p>`;
+
+    expect(sanitizeString(tainted).trimRight()).toBe(clean);
+  });
+});
diff --git a/server/sonar-web/src/main/js/helpers/sanitize.ts b/server/sonar-web/src/main/js/helpers/sanitize.ts
new file mode 100644 (file)
index 0000000..79b4a95
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { sanitize } from 'dompurify';
+
+export function sanitizeStringRestricted(html: string) {
+  return sanitize(html, {
+    ALLOWED_TAGS: ['b', 'br', 'code', 'i', 'li', 'p', 'strong', 'ul', 'a'],
+    ALLOWED_ATTR: ['target', 'href']
+  });
+}
+
+export function sanitizeString(html: string) {
+  return sanitize(html, { USE_PROFILES: { html: true } });
+}
index efc7915e8974cfdf6ab26b26fc637698ddedb142..5dbbc378f681b5604cd5f2e5598450467bd48027 100644 (file)
@@ -568,6 +568,17 @@ export function mockRule(overrides: Partial<T.Rule> = {}): T.Rule {
   } as T.Rule;
 }
 
+export function mockRuleActivation(overrides: Partial<T.RuleActivation> = {}): T.RuleActivation {
+  return {
+    createdAt: '2020-02-01',
+    inherit: 'NONE',
+    params: [{ key: 'foo', value: 'Bar' }],
+    qProfile: 'baz',
+    severity: 'MAJOR',
+    ...overrides
+  };
+}
+
 export function mockRuleDetails(overrides: Partial<T.RuleDetails> = {}): T.RuleDetails {
   return {
     key: 'squid:S1337',