Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FrontendFormRequest always gets default locale. #11179

Open
geertjanknapen1 opened this issue Nov 27, 2024 · 17 comments · May be fixed by #11194
Open

FrontendFormRequest always gets default locale. #11179

geertjanknapen1 opened this issue Nov 27, 2024 · 17 comments · May be fixed by #11194

Comments

@geertjanknapen1
Copy link

Bug description

My form submissions broke, always returning the error messages in our default language (English) even when on the German site.

I traced it to the following change > ec9e5c5

In my case $previousUrl is a storage link (myurl.test/storage/images/downloads/hobby/SDS_thumbnail_mobile.jpg), which does not contain a locale, which means Site::findByUrl() does not return an actual site.

This means it will fall back to the default language, causing the frontend errors to be in the wrong language.

How to reproduce

Setup a multisite with at least two languages. A form with multiple fields, and make sure you have the appropriate translation files.

Submit the form, using AJAX, and observer error messages returned in the default language, instead of the currently active language.

Logs

No response

Environment

Environment
Application Name: myapp
Laravel Version: 11.33.2
PHP Version: 8.3.13
Composer Version: 2.8.2
Environment: local
Debug Mode: ENABLED
URL: myapp.test
Maintenance Mode: OFF
Timezone: Europe/Amsterdam
Locale: en

Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: CACHED

Drivers
Broadcasting: log
Cache: file
Database: mysql
Logs: stack / daily, stderr
Mail: smtp
Queue: redis
Session: file

Statamic
Addons: 4
Sites: 8 (English, Dutch, Spanish, and 5 more)
Stache Watcher: Disabled
Static Caching: Disabled
Version: 5.39.0 PRO

Statamic Addons
appswithlove/statamic-one-click-content-translation: 5.2.3
spatie/statamic-responsive-images: 5.2.1
statamic-rad-pack/meilisearch: 3.3.0
withcandour/aardvark-seo: 5.0.0

Installation

Existing Laravel app

Additional details

No response

@geertjanknapen1
Copy link
Author

Update to add, when reverting the changes mentioned in the ticket, it no longer gets the storage URL, but the actual URL correctly, and the error messages show in the correct language.

@duncanmcclean
Copy link
Member

In my case $previousUrl is a storage link (myurl.test/storage/images/downloads/hobby/SDS_thumbnail_mobile.jpg), which does not contain a locale, which means Site::findByUrl() does not return an actual site.

What's the URL of the page you're submitting the form on?

You mentioned that you're submitting the form via AJAX, can you provide the code you're using?

@geertjanknapen1
Copy link
Author

The URL of the page I'm submitting the form on is either myapp.url/de/tools/shoplocator or myapp.url/de/hobby/produkte/<some_product>.

The AJAX looks as follows, it's all in a separate function which is called the moment the form-ajax-submit button is clicked;

let form = $(e.target).closest('form');
let formHtmlElement = form[0];
let formErrorSpan = form.find('span[data-failure]');
let failureMessage = formErrorSpan.data('failure');

// clear potential previously set validation errors
forms.clearValidationErrors(form);

// show spinning loader
$('#envelope-icon').addClass('d-none');
$('#spinning-loader').removeClass('d-none');

// Get the formData for this form. (Doing this in the AJAX call results in an Illegal Invocation error)
let formData = new FormData(formHtmlElement);

$.ajax({
        url: formHtmlElement.action,
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
    })
      .done(response => {
          if (response.success) {
              let confirmationRow = $('#confirmation-row');
              let formRow = $('#form-row');
              let formButton = $('.form-ajax-submit');

              confirmationRow.removeClass('d-none');
              formRow.addClass('d-none');
              formButton.addClass('d-none');

              $('html, body').stop(true,true).animate({
                  scrollTop: (confirmationRow.offset().top - 20)
              }, 500, 'linear');

              forms.enableSubmit(e);
          }
      })
      .fail(response => {
          if (response.status === 404) {
              console.log(response.responseJSON.message);

              formErrorSpan.html(failureMessage);
              formErrorSpan.show();
          } else {
              $.each(response.responseJSON.error, function (formFieldName, validationErrorText) {
                  let formField = form.find(`[name=${formFieldName}]`);
                  let formFieldValidationSpan = form.find(`span.${formFieldName}`);

                  formFieldValidationSpan.html(validationErrorText);
                  formFieldValidationSpan.show();
                  formField.addClass('is-invalid');
              });

              // This just enables the submit again on error.
              forms.enableSubmit(e);
          }
      });

We use the following ajaxSetup

$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
        'Accept-Language': $('html').attr('lang'),
        'X-Requested-With': 'XMLHttpRequest',
    },
});

We render forms in an Antlers PHP file, using

<div class="d-none" id="composed-view-data" data-countries='{{$ htmlentities($countries) $}}' data-languages='{{$ htmlentities($formSites) $}}'></div>
{{ form:no_shops_shoplocator name="no_shops_shoplocator" id="no-shops-shoplocator" }}
    <div class="row d-none" id="confirmation-row">
        <h2>{{ confirmation.title }}</h2>
        <p>{{ confirmation.content }}</p>
    </div>

    <!-- {{# if an error occurs that is not related to a specific field it will be shown here #}} -->
    <span class="invalid-feedback invalid-form" data-failure="{{ translate key='failure' }}"></span>

    <div class="row" id="form-row">
        <!-- {{# render all form fields that have been defined in the statamic blueprint.
             how formfields are rendered can be changed through antler files in /resources/views/vendor/statamic/forms/fields #}} -->
        {{ fields }}
            <div class="form-group col-{{$ percentageToColumn($width) $}}">
                <label for="{{ translate key="$handle" }}">
                    {{ translate key="$handle" }}
                    {{ if validate|contains:required }} * {{ /if }}
                </label>
                {{ field }}
                <span class="{{$ $handle $}} invalid-feedback"></span>
            </div>
        {{ /fields }}
    </div>

    <div class="w-100 mx-0 mt-5 mb-lg-0 row justify-content-end px-lg-0 px-3">
         <!-- {{# submit button, the 'form-ajax-submit' is picked up by jquery that will submit this form through ajax #}} -->
         <button class="btn col-12 col-lg-3 col-xl-2 btn-disabled RobotoMedium me-lg-3 form-ajax-submit" disabled>{{ translate key="submit" }}
             <i class="ms-2 fa fa-angle-right"></i>
             <i id="spinning-loader" class="fas fa-circle-notch fa-spin d-none"></i>
         </button>
    </div>

{{ /form:no_shops_shoplocator }}

@jasonvarga
Copy link
Member

Why would $previousUrl be an image? 🤔

@geertjanknapen1
Copy link
Author

geertjanknapen1 commented Nov 27, 2024

That's what I'm wondering as well @jasonvarga.

That specific image was throwing a 404, but for me, the behavior is reproducible on pages without 404's due to images. and on pages with other forms.

But I guess session()->previousUrl() as used in the new code can return storage urls, whilst URL::previous() as used before the change can not? But that's purely hypothetical.

I'll see if I can create a reproducible repo/zip-file sometime soon.

@ryanmitchell
Copy link
Contributor

Do you have multiple of these forms on a page and do they take you to the file on success? If so, is the error only happening after the first submission?

@geertjanknapen1
Copy link
Author

geertjanknapen1 commented Nov 28, 2024

Hey @ryanmitchell

There's only one of these forms on a page, at all times. There can be other forms on the same page, but these are not handled through Statamic.

Not quite sure what you mean with the following. What file are you referring to?

do they take you to the file on success?

@ryanmitchell
Copy link
Contributor

Ok - I was thinking if you had multiple forms on the page and it redirected to the file then it would set the previousUrl() to be that file, so when you next submitted the form it would get that value. I guess not.

@geertjanknapen1
Copy link
Author

Ah right, no that's not happening as far as I am aware. Upon success, I can see the submission in the control panel and I get the e-mail delivered to my mailbox, so that's all working as intended.

The problem is really with the displaying of localized error messages.

@duncanmcclean
Copy link
Member

duncanmcclean commented Nov 28, 2024

Are you able to replicate this issue on a fresh site or provide access to the site where you're experiencing this issue?

@geertjanknapen1
Copy link
Author

Heya @duncanmcclean

Did some more tinkering. It did happen due to the 404 on an image, which causes $previousUrl to take the value of the URL that is 404'ing. (I could swear I saw it happening without the 404, but I can't reproduce it, and it could have been caused by cache).

I've taken some time to create a minimal reproducible example on a fresh site, where I can reproduce it by creating an img element with a non-existing image.
If that element is not present, everything works as expected. If it is present, the validation errors will no longer be translated.

I'm setting up the Github repo right now, will comment in a minute.

@ryanmitchell
Copy link
Contributor

That would make sense as the missing image would hit the Statamic/Laravel session handler and so set the last URL.

@geertjanknapen1
Copy link
Author

@ryanmitchell That does make sense, but it doesn't do that if I manually reverse the changes made in ec9e5c5 then everything works as expected.

Even with the missing image, the messages will be properly translated.

@duncanmcclean My Github SSH keys are giving me grief, so here's a ZIP archive containing the example app.
statamic-reproducible-example.zip

@duncanmcclean
Copy link
Member

It did happen due to the 404 on an image

Technically, it's working as expected from Statamic's perspective. It's using whatever the last page you visited was to determine the site.

In your case, since that's a 404'ing image, it hits Statamic/Laravel and it gets counted as the "previous page". If you fix the 404'ing image, it should work fine.

@geertjanknapen1
Copy link
Author

Right, but that does not happen prior to those changes, and it's kind of weird from a user's perspective to see un-translated error messages just because there is an image missing.

But, seeing that you've closed the issue, I guess that means you'll not be looking into it further.

@duncanmcclean duncanmcclean reopened this Nov 28, 2024
@duncanmcclean
Copy link
Member

Actually, I have an idea of how we can fix it while not breaking things for headless sites (which was the reason for that change).

@geertjanknapen1
Copy link
Author

I took the liberty of testing the changes you proposed, works like a charm, thanks!

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

Successfully merging a pull request may close this issue.

4 participants