Skip to content

Only email and username user attributes are allowed in users-permissions email templates #19442

@mikaelpopowicz

Description

@mikaelpopowicz

Bug report

Required System information

  • Node.js version: v20.0.9
  • NPM version: 10.1.0
  • Strapi version: 4.19.1
  • Database: sqlite
  • Operating system: Macos
  • Is your project Javascript or Typescript: Javascript

Describe the bug

When using the plugin users-permissions to update one the provided email templates we cannot use custom fields from the user schema.

Indeed we can see that the authorized keys only contain USER.email and USER.username.

Steps to reproduce the behavior

  1. Go to Settings
  2. Go to Email templates
  3. Click on one of the email templates (Reset password or Email address confirmation)
  4. Fill the Message field with <%= USER.firstName %> (or any other configured field in the User content type)
  5. See error Invalid template

Expected behavior

Actually I guess this is the expected behavior but we may want to be able to use any field from user schema.

Mitigating the issue

We can override this behavior to be able to use any field from user schema.

Create a file ./src/extensions/users-permissions/overrides.js which is a copy of the original function where we use our custom email validation

'use strict';

const _ = require("lodash");
// Addition : use our custom validation
const { isValidEmailTemplate } = require("./email-template");

module.exports = {
  async updateEmailTemplate(ctx) {
    if (_.isEmpty(ctx.request.body)) {
      throw new ValidationError('Request body cannot be empty');
    }

    const emailTemplates = ctx.request.body['email-templates'];

    for (const key of Object.keys(emailTemplates)) {
      const template = emailTemplates[key].options.message;

      if (!isValidEmailTemplate(template)) {
        throw new ValidationError('Invalid template');
      }
    }

    await strapi
      .store({type: 'plugin', name: 'users-permissions', key: 'email'})
      .set({value: emailTemplates});

    ctx.send({ok: true});
  }
}

Create a file ./src/extensions/users-permissions/email-template.js which is a copy of the original rule where we change the authorizedKeys array

'use strict';

const { trim } = require('lodash/fp');
const {
  template: { createLooseInterpolationRegExp, createStrictInterpolationRegExp },
} = require('@strapi/utils');

const invalidPatternsRegexes = [
  // Ignore "evaluation" patterns: <% ... %>
  /<%[^=]([\s\S]*?)%>/m,
  // Ignore basic string interpolations
  /\${([^{}]*)}/m,
];

// Addition : get the user schema
const userSchema = strapi.getModel('plugin::users-permissions.user');

const authorizedKeys = [
  'URL',
  'ADMIN_URL',
  'SERVER_URL',
  'CODE',
  'USER',
  // Addition : spread user attributes
  ...Object.entries(userSchema.attributes).map(([key, value]) => `USER.${key}`),
  'TOKEN',
];

const matchAll = (pattern, src) => {
  const matches = [];
  let match;

  const regexPatternWithGlobal = RegExp(pattern, 'g');

  // eslint-disable-next-line no-cond-assign
  while ((match = regexPatternWithGlobal.exec(src))) {
    const [, group] = match;

    matches.push(trim(group));
  }

  return matches;
};

const isValidEmailTemplate = (template) => {
  // Check for known invalid patterns
  for (const reg of invalidPatternsRegexes) {
    if (reg.test(template)) {
      return false;
    }
  }

  const interpolation = {
    // Strict interpolation pattern to match only valid groups
    strict: createStrictInterpolationRegExp(authorizedKeys),
    // Weak interpolation pattern to match as many group as possible.
    loose: createLooseInterpolationRegExp(),
  };

  // Compute both strict & loose matches
  const strictMatches = matchAll(interpolation.strict, template);
  const looseMatches = matchAll(interpolation.loose, template);

  // If we have more matches with the loose RegExp than with the strict one,
  // then it means that at least one of the interpolation group is invalid
  // Note: In the future, if we wanted to give more details for error formatting
  // purposes, we could return the difference between the two arrays
  if (looseMatches.length > strictMatches.length) {
    return false;
  }

  return true;
};

module.exports = {
  isValidEmailTemplate,
};

Now we can override the controller function in ./src/index.js

'use strict';

module.exports = {
  /**
   * An asynchronous register function that runs before
   * your application is initialized.
   *
   * This gives you an opportunity to extend code.
   */
  register({ strapi }) {
    strapi.controllers['plugin::users-permissions.settings'].updateEmailTemplate = require('./extensions/users-permissions/overrides').updateEmailTemplate;
  },

  /**
   * An asynchronous bootstrap function that runs before
   * your application gets started.
   *
   * This gives you an opportunity to set up your data model,
   * run jobs, or perform some special logic.
   */
  bootstrap(/*{ strapi }*/) {},
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions