Skip to content

Fix mask plugin with data directive (CSP)#4090

Closed
kg-currenxie wants to merge 2 commits intoalpinejs:mainfrom
kg-currenxie:patch-1
Closed

Fix mask plugin with data directive (CSP)#4090
kg-currenxie wants to merge 2 commits intoalpinejs:mainfrom
kg-currenxie:patch-1

Conversation

@kg-currenxie
Copy link
Copy Markdown

@kg-currenxie kg-currenxie commented Mar 12, 2024

Before:

Data directive:

Alpine.data('form', function() {
  return {
    amountMask: function (inputValue) {
      return this.$money(inputValue) // ❌ How to access the $money formatter?
    }
  }
})

HTML:

<form x-data="form">
  <input x-mask:dynamic="amountMask($input, $money)" /> // ❌ 
</form>

image

After

Data directive:

Alpine.data('form', function() {
  return {
    amountMask: function (inputValue, moneyMaskFormat) { // ✅ We get access to the formatter
      return moneyMaskFormat(inputValue)
    }
  }
})

HTML:

<form x-data="form">
  <input x-mask:dynamic="amountMask" /> // ✅ Valid CSP expression
</form>

Let me know if I'm missing something, or if it would break something else?
It does work in our app's UI tho.

@kg-currenxie
Copy link
Copy Markdown
Author

I can't figure out what causes the breaking tests really :) Help appreciated 🙏

This is the last bit of a huge CSP re-write, to deploy our new app to production :)

@ekwoka
Copy link
Copy Markdown
Contributor

ekwoka commented Mar 12, 2024

I think the improved API would probably be actually binding the function to an object with the scope/magics...

also not sure why it causes an issue with the failed tests...but definitely need to figure that one out...

@kg-currenxie
Copy link
Copy Markdown
Author

kg-currenxie commented Mar 12, 2024

binding the function to an object with the scope/magics...

not following a 100% :)

do you mean passing this as the 2nd argument instead? 🤔

{
  '$input': input,
  '$money': formatMoney.bind({ el }),
}

or

value(input).bind({
                                '$input': input,
                                '$money': formatMoney.bind({ el }),
                            })

🤔

@ekwoka
Copy link
Copy Markdown
Contributor

ekwoka commented Mar 12, 2024

more like

value.bind({ $input: input, $money: formatMoney.bind({el}) })(input)

But also whatever would make this refer to the data context as well...

@kg-currenxie
Copy link
Copy Markdown
Author

But also whatever would make this refer to the data context as well...

Yea, i just came across that issue. this would be undefined. Haven't solved that yet 😓

@ekwoka
Copy link
Copy Markdown
Contributor

ekwoka commented Mar 12, 2024

Well, I just showed....bind the value...

@kg-currenxie
Copy link
Copy Markdown
Author

Sorry, i spoke too soon :P

this now have access to $input and $money 👌 but not the data directive :/ which i need in my case.

basically im doing something like

      this.$money(
        this.$input,
        '.',
        ',',
        this.nonFractionCurrencies.includes(this.currency) ? 0 : 2,
      )

where nonFractionCurrencies and currency is stored in the data context

thanks for you continuing support! 🙏

@ekwoka
Copy link
Copy Markdown
Contributor

ekwoka commented Mar 12, 2024

You can possibly look at how magics are injected into the state in the first place, or use mergeProxies directly to layer them.

@kg-currenxie kg-currenxie marked this pull request as draft March 13, 2024 09:08
@calebporzio
Copy link
Copy Markdown
Collaborator

PR Review: #4090 — Fix mask plugin with data directive (CSP)

Type: Bug fix
Verdict: Close

What's happening (plain English)

  1. The contributor uses CSP mode (no eval), so they can't write inline expressions like x-mask:dynamic="$money($input, ',')".
  2. They want to use Alpine.data() to define a named mask function like amountMask, then reference it with x-mask:dynamic="amountMask".
  3. The mask plugin already provides $money and $input as "magics" via the evaluator scope (line 22-26 of packages/mask/src/index.js). In the standard evaluator, these are accessible when the expression is evaluated.
  4. The problem: when the expression resolves to a function (like amountMask from Alpine.data()), it's called with just value(input) — the scope magics aren't passed through.
  5. The PR's fix: pass formatMoney.bind({ el }) as a hardcoded second argument to the function.

Why this fix is wrong

The change is:

// Before:
result = typeof value === 'function' ? value(input) : value
// After:  
result = typeof value === 'function' ? value(input, formatMoney.bind({ el })) : value

Problems:

  1. Hardcodes $money as a second arg. This only helps the $money use case. If someone wanted access to other magics or scope values, they're out of luck. It's not a generic solution.
  2. Changes the function signature contract for all users. Every x-mask:function callback now silently receives a second argument. This is a public API change that affects all mask functions, not just CSP ones.
  3. $money is already available in scope. The mask plugin already injects $money into the evaluator scope on line 25. The real issue is that the scope context isn't passed as this when calling the resolved function — that's what should be fixed if this were to be addressed.
  4. The discussion in the PR comments (by @ekwoka) already pointed toward the right approach: binding the function to an object containing the scope/magics, not passing them as positional args. Something like value.call(scopeWithMagics, input) or value.bind(scopeWithMagics)(input) would let the data function access this.$money, this.$input, etc.

Other approaches considered

  1. Bind the function to the scope contextvalue.call(scope, input) where scope includes magics. This gives the function access to this.$money, this.$input, and all data properties. This is the correct approach if this feature is needed.
  2. Do nothing — in CSP mode, the contributor could define formatMoney logic directly in their Alpine.data() without relying on the mask plugin's $money magic. The mask plugin is designed for template expressions; the CSP limitation here is real but narrow.
  3. This PR's approach — hardcode formatMoney as a second arg. Wrong for the reasons above.

Changes Made

No changes made. The fix is architecturally wrong.

Test Results

  • No tests included in the PR.
  • No CI checks (PR from March 2024, branch has no status checks).
  • The contributor acknowledged failing tests they couldn't resolve.

Code Review

The single-line change at packages/mask/src/index.js:21 breaks the function signature contract and is a hardcoded workaround for a narrow use case. The discussion in PR comments between the contributor and @ekwoka shows this wasn't fully worked through.

Security

No security concerns identified.

Verdict

Close. This PR is almost 2 years old with no maintainer approval, no tests, acknowledged broken tests, and the implementation approach is fundamentally wrong. The real fix (if needed) would be to bind the resolved function to the evaluator scope so this.$money etc. are accessible — not to pass formatMoney as a hardcoded positional argument.

The use case (CSP + Alpine.data() + x-mask:dynamic needing access to $money) is legitimate but narrow. If this needs to be addressed, the right approach is value.call(scopeWithMagics, input) where scopeWithMagics includes the $money and $input magics. That gives data functions access to everything via this, which is consistent with how Alpine data functions work everywhere else.


Reviewed by Claude

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants