Skip to content

Settings CJS mirror is built before reloadSettings(), so plugin settings hashes are never visible to CJS plugins #8109

Description

@mathewcsims

Summary

#7421 added property getters on module.exports so that plugins using
require('ep_etherpad-lite/node/utils/Settings') can read settings directly.
That works for built-in settings such as toolbar, but not for plugin-specific
settings hashes
(ep_* keys) supplied via settings.json.

The mirror is built from a snapshot of Object.keys(settings) taken at
module-evaluation time, which happens before reloadSettings() parses
settings.json and adds the plugin keys. A plugin reading its own configuration
the documented way therefore always sees undefined.

Observed on 3.3.3, and the same ordering is present in 2.7.3.

Root cause

In src/node/utils/Settings.ts:

  • line ~912 — the CJS compatibility block from Fix ESM/CJS interop for Settings module breaking plugin compatibility #7421:
    if (typeof module !== 'undefined' && module.exports) {
      const currentExports = module.exports;
      for (const key of Object.keys(settings)) {
        if (!(key in currentExports)) {
          Object.defineProperty(currentExports, key, { get, set, ... });
        }
      }
    }
  • line ~1464reloadSettings() is called, which runs storeSettings() and
    adds ep_* keys from settings.json.

Object.keys(settings) at line 912 contains only the defaults declared in the
module. ep_comments_page (or any ep_* hash) is not a default — it appears 550
lines later — so no getter is ever defined for it. The getters themselves are
live, so existing keys track correctly; the problem is purely which keys get
mirrored.

Reproduction

  1. Add a plugin settings hash to settings.json:
    { "ep_comments_page": { "allowReadonlyComments": true } }
  2. In any plugin, log what the documented accessor returns:
    const mod = require('ep_etherpad-lite/node/utils/Settings');
    console.log(mod.ep_comments_page);                       // undefined
    console.log(mod.default && mod.default.ep_comments_page); // the object
    console.log(Object.keys(mod).filter(k => k.startsWith('ep_')));         // []
    console.log(Object.keys(mod.default).filter(k => k.startsWith('ep_'))); // ['ep_comments_page']

Actual probe output from a stock 3.3.3 container with the above settings.json:

{"moduleType":"object","hasDefault":true,
 "default_ep_comments_page":{"allowReadonlyComments":true,"displayCommentAsIcon":true},
 "argsSettings_ep":{"allowReadonlyComments":true,"displayCommentAsIcon":true},
 "ep_keys_on_module":[],
 "ep_keys_on_default":["ep_comments_page"]}

No Unknown Setting warning is logged — storeSettings() accepts ep_* keys
correctly. The value is loaded; it just isn't reachable through the CJS mirror.

The same happens via the EP__ep_comments_page__allowReadonlyComments=true
environment variable, so it isn't specific to how the setting is supplied.

Impact

Any plugin that reads its own configuration with
require('ep_etherpad-lite/node/utils/Settings').ep_myplugin silently sees
undefined, so every configurable option of that plugin falls back to its
default with no error or warning. This is the pattern #7421 was intended to keep
working.

Concretely: ep_comments_page's allowReadonlyComments and
displayCommentAsIcon cannot be enabled at all on a stock install.

Suggested fix

Either move the CJS mirror block to after reloadSettings(), or replace the
key-by-key snapshot with a Proxy that forwards property access to settings,
so keys added later are covered without depending on evaluation order.

Workarounds that do work today, for reference: read .default (or use
.default || mod), or take args.settings from the loadSettings hook.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions