Send/Upload files?

Created on 4 August 2017, over 7 years ago
Updated 2 March 2023, almost 2 years ago
โœจ Feature request
Status

RTBC

Version

4.0

Component

Miscellaneous

Created by

๐Ÿ‡ฉ๐Ÿ‡ชGermany FNGR

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • ๐Ÿ‡ธ๐Ÿ‡ชSweden emdahlstrom

    Hi! Any update on getting this into a release?

  • Status changed to Needs work over 1 year ago
  • ๐Ÿ‡ฌ๐Ÿ‡ตGuadeloupe Monster971

    Warning : Do โ€‹โ€‹not install the Webform REST 4.0.2 update, if you have this patch.

    This completely broke my project in production.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia imclean Tasmania
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States arrow Chicago

    @Monster971 did you apply this patch to version 4.0.2 before uploading it to production? Did it apply to 4.0.2 successfully and cause issues? Did it fail to apply? Is further work needed on the code contained in the patch? Please try to be helpful with your comments. Open-source software is built by all of us working together to make things better.

  • ๐Ÿ‡ฌ๐Ÿ‡ตGuadeloupe Monster971

    Hello @Arrow,

    Sorry for the lack of additional information to help the community.

    I have a Drupal 9.5.5 (Apache/2.4.6 (CentOS) PHP/7.4.33) project that relies heavily on Webform REST module patch #37.

    When I tried to update Webform REST 4.0.1 to Webform 4.0.2 with the following command:

    composer update drupal/webform_rest

    I got the following error:

    Could not apply patch! Skipping. The error was: Cannot apply patch https://www.drupal.org/files/issues/2021-04-30/webform_rest-add_file_upload_resource-2899902-37.patch

    As well as in my Drupal project the following PHP error which crashes my project:

    Drupal\Component\Plugin\Exception\PluginException: Plugin (webform_rest_file_upload) instance class "Drupal\webform_rest\Plugin\rest\resource\WebformFileUploadResource" does not exist. in Drupal\Component\Plugin\Factory\DefaultFactory::getPluginClass() (line 97 of /var/www/html/myproject/web/core/lib/Drupal/Component/Plugin/Factory/DefaultFactory.php).

    Here is the information from my side.

  • ๐Ÿ‡ง๐Ÿ‡ฌBulgaria rhristov

    Updating the patch from #47 to work with Drupal 10 where MimeTypeGuesser::guess was removed and MimeTypeGuesser:: guessMimeType is used.

    https://www.drupal.org/node/3126004 โ†’ for reference.

  • Open in Jenkins โ†’ Open on Drupal.org โ†’
    Core: 10.0.7 + Environment: PHP 8.1 & MariaDB 10.3.22
    last update over 1 year ago
    4 fail
  • Status changed to Needs review over 1 year ago
  • ๐Ÿ‡ฎ๐Ÿ‡นItaly robertom

    Attached re-rolled patch that include changes from #55 for 4.0.2 and 4.x version

  • Open in Jenkins โ†’ Open on Drupal.org โ†’
    Core: 9.5.x + Environment: PHP 8.1 & MariaDB 10.3.22
    last update over 1 year ago
    run-tests.sh fatal error
  • Open in Jenkins โ†’ Open on Drupal.org โ†’
    Core: 10.0.7 + Environment: PHP 8.1 & MariaDB 10.3.22
    last update over 1 year ago
    Patch Failed to Apply
  • Open in Jenkins โ†’ Open on Drupal.org โ†’
    Core: 10.0.7 + Environment: PHP 8.1 & MariaDB 10.3.22
    last update over 1 year ago
    4 fail
  • ๐Ÿ‡ช๐Ÿ‡ฌEgypt Mohamed Nabawy

    rerolled patch to work with v4.0.3

  • ๐Ÿ‡ฌ๐Ÿ‡ตGuadeloupe Monster971

    Hello,

    I have a question, for a long time I was using patch #42 which saved my files in /files/private/webform/webform_id/, a week ago I applied patch #56-v4.x, I found that the files are now saved in /files/private/webform_id_tmp/.

    Is this normal?

  • ๐Ÿ‡ช๐Ÿ‡ฌEgypt Mohamed Nabawy

    enhanced code for validation implements

  • Open in Jenkins โ†’ Open on Drupal.org โ†’
    Core: 9.5.x + Environment: PHP 5.3 & MySQL 5.5
    last update over 1 year ago
    Composer error. Unable to continue.
  • ๐Ÿ‡ง๐Ÿ‡ฌBulgaria rhristov

    Updating the patch from #59 to replace a deprecated function call MimeTypeGuesser::guess() with MimeTypeGuesser::guessMimeType().

  • Open in Jenkins โ†’ Open on Drupal.org โ†’
    Core: 9.5.x + Environment: PHP 8.1 & MySQL 8
    last update over 1 year ago
    run-tests.sh fatal error
  • First commit to issue fork.
  • ๐Ÿ‡น๐Ÿ‡ผTaiwan johnalbin Taipei, Taiwan

    The patch in #61 is malformed because your .editorconfig stripped trailing spaces from the file. I hate when that happens. :p

    I pushed @rhristov's change to GitLab.

  • ๐Ÿ‡ง๐Ÿ‡ฌBulgaria tolibg

    I am sending attached files successfully from NextJs to Drupal Webform. When I try to preview the files in Drupal the PDF files are perfect but .doc and .docx are corrupted.
    Any thoughts what can be the problem?

  • ๐Ÿ‡จ๐Ÿ‡ฆCanada othermachines Edmonton, Alberta

    @tolibg Same problem here. We think that all file formats are affected but PDFs possibly more forgiving. A coworker is checking into it.

    Also we need to remove \Drupal\Component\Utility\Bytes::toInt() (change record linked below) for Drupal 10 compatibility. Uploads are failing for us without this change.

    \Drupal\Component\Utility\Bytes::toInt() is deprecated in favor of \Drupal\Component\Utility\Bytes::toNumber()

  • ๐Ÿ‡จ๐Ÿ‡ฆCanada othermachines Edmonton, Alberta

    Sorry, my brain is tired from all of the Drupal 10 migrations!! I think another version check may still be needed on the second occurrence of toInt() (line 203).

  • First commit to issue fork.
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium kensae

    I've added the check suggested in #66

  • ๐Ÿ‡ฌ๐Ÿ‡ทGreece sl45sms

    fyi Bytes:toInt is deprecated on drupal10
    so ive mannualy patch WebformFileUplodResource.php->getElementValidators

      protected function getElementValidators(array $element) {
        $validators = [
          // Add in our check of the file name length.
          'file_validate_name_length' => [],
        ];
    
        // Cap the upload size according to the PHP limit.
        if (version_compare(\Drupal::VERSION, '9.1', '<')) {
          $max_filesize = Bytes::toInt(Environment::getUploadMaxSize());
        }
        else {
          $max_filesize = Bytes::toNumber(Environment::getUploadMaxSize());
        }
    
        if (version_compare(\Drupal::VERSION, '9.1', '<')) {
        if (!empty($element["#max_filesize"])) {
          $max_filesize = min(
            $max_filesize,
            Bytes::toInt($element['#max_filesize'] * 1024 * 1024)
          );
        }
        }
        else {
          if (!empty($element["#max_filesize"])) {
            $max_filesize = min(
              $max_filesize,
              Bytes::toNumber($element['#max_filesize'] * 1024 * 1024)
            );
          }
        }
    
        // There is always a file size limit due to the PHP server limit.
        $validators['file_validate_size'] = [$max_filesize];
    
        // Add the extension check if necessary.
        if (!empty($element['#file_extensions'])) {
          $validators['file_validate_extensions'] = [$element['#file_extensions']];
        }
    
        return $validators;
      }
    
  • ๐Ÿ‡จ๐Ÿ‡ฆCanada othermachines Edmonton, Alberta

    @sl45sms Super but this was already addressed in #67.

  • ๐Ÿ‡ฌ๐Ÿ‡ทGreece sl45sms

    @othermachines yep, late nite, brain fog, and lost in thousand drupal 10 patches I missed that. Thanks.

  • I successfully applied the webform_rest-add_file_upload_resource-2899902-61.patch #61 on Drupal Version 9.5.10 and PHP Version 8.1.26, and it worked fine. Thank you for the patch!

    However, when testing on Drupal Version 10.1.6, I encountered an error related to Bytes:toInt. The issue seems to be resolved with the update in patch #69.

      protected function getElementValidators(array $element) {
        $validators = [
          // Add in our check of the file name length.
          'file_validate_name_length' => [],
        ];
    
        // Cap the upload size according to the PHP limit.
        if (version_compare(\Drupal::VERSION, '9.1', '<')) {
          $max_filesize = Bytes::toInt(Environment::getUploadMaxSize());
        }
        else {
          $max_filesize = Bytes::toNumber(Environment::getUploadMaxSize());
        }
    
        if (version_compare(\Drupal::VERSION, '9.1', '<')) {
        if (!empty($element["#max_filesize"])) {
          $max_filesize = min(
            $max_filesize,
            Bytes::toInt($element['#max_filesize'] * 1024 * 1024)
          );
        }
        }
        else {
          if (!empty($element["#max_filesize"])) {
            $max_filesize = min(
              $max_filesize,
              Bytes::toNumber($element['#max_filesize'] * 1024 * 1024)
            );
          }
        }
    
        // There is always a file size limit due to the PHP server limit.
        $validators['file_validate_size'] = [$max_filesize];
    
        // Add the extension check if necessary.
        if (!empty($element['#file_extensions'])) {
          $validators['file_validate_extensions'] = [$element['#file_extensions']];
        }
    
        return $validators;
      }

    Thank you for the continued efforts in maintaining and improving this patch.

    Best regards,

    malikarslanasif131

  • ๐Ÿ‡ง๐Ÿ‡ฌBulgaria pfrenssen Sofia
  • ๐Ÿ‡ง๐Ÿ‡ฌBulgaria pfrenssen Sofia
  • ๐Ÿ‡ฆ๐Ÿ‡ฒArmenia as_vard Yerevan

    Has anyone faced the file upload size validation issue? I set the webform field file upload max size to 5MB, but it allows sending files larger than 5 MB.

  • Status changed to Needs work 11 months ago
  • ๐Ÿ‡ง๐Ÿ‡ฌBulgaria pfrenssen Sofia

    We need to add an access check, in the current implementation everybody can upload whatever files they want through this endpoint, even anonymous users.

  • ๐Ÿ‡ง๐Ÿ‡ฌBulgaria pfrenssen Sofia

    @as_vard I just checked this with the latest version of the MR and it seems to work as expected. I am getting a 422 error with the following message:

    Unprocessable Entity: file validation failed. The file is 2 MB exceeding the maximum file size of 1 MB.

    I am working on tests. I will make sure to add coverage for uploading invalid files.

  • Status changed to Needs review 11 months ago
  • ๐Ÿ‡ง๐Ÿ‡ฌBulgaria pfrenssen Sofia

    Added tests, addressed review remarks, fixed the security hole. This is ready for review again.

  • ๐Ÿ‡ง๐Ÿ‡ฌBulgaria pfrenssen Sofia

    The tests are not running because our configuration is outdated. I created a ticket to restore the tests: ๐Ÿ› Restore test runs Needs review .

  • ๐Ÿ‡ฆ๐Ÿ‡ฒArmenia as_vard Yerevan

    pfrenssen โ†’ thanks for your replay. But even after the last changes, I am still can upload files larger than the maximum file size for that field and upload files whose format is not accepted for those fields. I am using Drupal 10.2.1. Tested on Postman: Screenshot
    Could you please describe the steps that you use to test the webform_rest file upload API?

  • ๐Ÿ‡ง๐Ÿ‡ฌBulgaria pfrenssen Sofia

    @as_vard, thanks for the report. I wrote the tests using the standard REST testing. It works with Guzzle which uses Curl under the hood. Here is the test for an upload that exceeds the file size limit: https://git.drupalcode.org/project/webform_rest/-/merge_requests/9/diffs...

    Do you get a failure when you run the tests?

  • ๐Ÿ‡ฆ๐Ÿ‡ฒArmenia as_vard Yerevan

    @pfrenssen โ†’ I did not run the tests.

  • Hello, I am using this patch on D10.1.5, I followed the documentation step by step, but when I try to send the file I got following error:

    Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException: No route found for "GET http://example.com/webform_rest/job_applications/upload/cv": Method Not Allowed (Allow: POST) in Symfony\Component\HttpKernel\EventListener\RouterListener->onKernelRequest() (line 131 of /var/www/html/vendor/symfony/http-kernel/EventListener/RouterListener.php).

    Has anyone faced this before, I appreciate any suggestions?

  • ๐Ÿ‡จ๐Ÿ‡ฆCanada quadbyte

    @gdurglishvili Error seems pretty explicit and is not in the module but in the code you use to send the file.
    Server is expecting a POST request, and you are doing a GET.

  • ๐Ÿ‡จ๐Ÿ‡ฆCanada quadbyte

    Validation code seems indeed suspicious. I'm getting "Array to string conversion" warnings.
    It seems we are sending to validation an array of validators instead of an array of fields?
    Don't know much about validation, so I might be totally wrong.

    Warning: Array to string conversion in Drupal\file\Plugin\rest\resource\FileUploadResource->validate() (line 45 of /var/www/experience/dev/web/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php)
    #0 /var/www/experience/dev/web/core/includes/bootstrap.inc(164): _drupal_error_handler_real()
    #1 [internal function]: _drupal_error_handler()
    #2 /var/www/experience/dev/web/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php(45): array_diff()
    #3 /var/www/experience/dev/web/modules/contrib/webform_rest/src/Plugin/rest/resource/WebformFileUploadResource.php(129): Drupal\file\Plugin\rest\resource\FileUploadResource->validate()
    #4 [internal function]: Drupal\webform_rest\Plugin\rest\resource\WebformFileUploadResource->post()
    
  • ๐Ÿ‡จ๐Ÿ‡ฆCanada quadbyte

    FYI, patch no longer works with Drupal 10.3.0. I have not had the time to look into.

    Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: You have requested a non-existent service 'mime_type.guesser'

  • ๐Ÿ‡ณ๐Ÿ‡ฑNetherlands markwittens

    I cannot clone branch 2899902-sendupload-files so I've created a new (full) patch based on 4.x, with the changes from branch 2899902-sendupload-files and a fix for D10.3.

  • ๐Ÿ‡ณ๐Ÿ‡ฑNetherlands markwittens

    It's probably useful to attach the patch as well.

  • First commit to issue fork.
  • First commit to issue fork.
  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany gooddev

    and for people who do not know, creating a patch from a MR is simple, just add the suffix ".patch" to the actual MR.

    so this here is the patch: https://git.drupalcode.org/project/webform_rest/-/merge_requests/20.patch

  • ๐Ÿ‡ณ๐Ÿ‡ฑNetherlands markwittens

    Is this upload endpoint working for you for files with special characters in them? I.e. "filenameโ„ข.pdf".

    To be able to upload files with special characters in the filename, the filename in the Content-Disposition header should be encoded or it will fail even before uploading:

    "Content-Disposition": `file; filename="${ encodeURI(file.name) }"`,

    When I do this and upload a file with spaces in it, the filename is saved in Drupal with %20 instead of spaces, and as a result it throws an error when trying to download it:

    InvalidArgumentException: The filename fallback cannot contain the "%" character. in Symfony\Component\HttpFoundation\HeaderUtils::makeDisposition() (line 182 of /app/vendor/symfony/http-foundation/HeaderUtils.php).
    

    When I remove the encodeURI part, it works for most files but the POST will fail for files with special characters in them:

    Cannot convert argument to a ByteString because the character at index 52 has a value of 8482 which is greater than 255.

  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany gooddev

    @markwittens that is exactly what i do, but inside by external typescript application.

    fileUploadHeaders.append( 'Content-Disposition',`file; filename="${encodeURIComponent(fileData.file.name)}"`);
    

    I tested it with your example filename, and it works like a charm :)

    Maybe you should concider rename it directly in the file webform field by using tokens?

  • ๐Ÿ‡ณ๐Ÿ‡ฑNetherlands markwittens

    @gooddev Are you renaming the file uploads using the "Rename files" option on the file upload component in Webforms? That might be the difference and why it works for you and not for me.

    I've I added a patch for the issue branch that fixes the issue by decoding the filename before saving it. The patch also cleans up the function a bit and a I have replaced a deprecated function. Can you try if this works for your case as well?

  • ๐Ÿ‡ฒ๐Ÿ‡ฆMorocco zrigou

    I am currently working on a feature that involves submitting data to Drupal using the FormData format, particularly for forms that include file uploads. However, I've noticed that while data sent as JSON is processed correctly, the data sent using FormData is not being accepted by Drupal.

    Could you please advise if there are any specific configurations or steps required on the Drupal side to ensure that FormData submissions are handled correctly? I want to ensure that I'm following best practices for handling such data submissions.

    import { GetStaticPaths, GetStaticProps } from "next";
    import { DrupalClient, DrupalNode } from "next-drupal";
    import { useState, FormEvent } from "react";
    import { useTranslation } from "next-i18next";
    import { useRouter } from "next/router";
    
    import { Body } from "@/components/body";
    import { HeadingPage } from "@/components/heading--page";
    import { Meta } from "@/components/meta";
    import {
      CommonPageProps,
      getCommonPageProps,
    } from "@/lib/get-common-page-props";
    
    import { env } from "@/env";
    
    interface MetaTag {
      tag: string;
      attributes: {
        name?: string;
        content?: string;
        rel?: string;
        href?: string;
      };
    }
    
    interface careersProps extends CommonPageProps {
      career: DrupalNode;
    }
    
    export default function careers({ career }: careersProps) {
        console.log('career' , career);
    
      const { t } = useTranslation();
      const [submissionStatus, setSubmissionStatus] = useState<"success" | "error" | null>(null);
      const router = useRouter();
    
      const imageLink = (career.metatag as MetaTag[] | undefined)?.find(
        (meta: MetaTag) =>
          meta.tag === "link" && meta.attributes.rel === "image_src",
      )?.attributes.href;
    
      async function handleSubmit(event: FormEvent<HTMLFormElement>) {
        event.preventDefault();
        
        const formData = new FormData(event.currentTarget);
        formData.append('webform_id', 'careers');
        formData.append('entity_id', career.id);
    
        try {
          const response = await fetch(
            `${env.NEXT_PUBLIC_DRUPAL_BASE_URL}/webform_rest/submit`,
            {
              method: "POST",
              body: formData,
            }
          );
    
          if (response.ok) {
            setSubmissionStatus("success");
            setTimeout(() => router.push('/careers'), 3000);
            console.log('Submission successful:', await response.json());
          } else {
            setSubmissionStatus("error");
            console.error('Submission error:', response.statusText);
          }
        } catch (error) {
          setSubmissionStatus("error");
          console.error('Submission failed:', error);
        }
      }
    
      return (
        <>
          <Meta title={career.title} metatags={[]} />
          <HeadingPage>{career.title}</HeadingPage>
          <div className="mt-8 space-y-6">
            {imageLink && (
              <div className="overflow-hidden rounded-lg shadow-lg">
                <img
                  src={imageLink}
                  alt={career.title}
                  className="w-full h-auto object-cover"
                />
              </div>
            )}
            <div className="prose max-w-none">
              <Body value={career.body.processed} />
            </div>
            {submissionStatus === "success" ? (
              <div className="p-4 text-green-700 bg-green-100 border border-green-700 rounded">
                {t("Your application has been submitted successfully.")}
              </div>
            ) : submissionStatus === "error" ? (
              <div className="p-4 text-red-700 bg-red-100 border border-red-700 rounded">
                {t("There was an error submitting your application. Please try again.")}
              </div>
            ) : (
              <form onSubmit={handleSubmit} className="space-y-6">
                <div>
                  <label className="block text-sm font-medium text-gray-700" htmlFor="subject">{t("Subject")}</label>
                  <input
                    type="text"
                    id="subject"
                    name="subject"
                    required
                    className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                  />
                </div>
                <div>
                  <label className="block text-sm font-medium text-gray-700" htmlFor="firstName">{t("First Name")}</label>
                  <input
                    type="text"
                    id="firstName"
                    name="firstName"
                    required
                    className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                  />
                </div>
                <div>
                  <label className="block text-sm font-medium text-gray-700" htmlFor="lastName">{t("Last Name")}</label>
                  <input
                    type="text"
                    id="lastName"
                    name="lastName"
                    required
                    className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                  />
                </div>
                <div>
                  <label className="block text-sm font-medium text-gray-700" htmlFor="email">{t("Email")}</label>
                  <input
                    type="email"
                    id="email"
                    name="email"
                    required
                    className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                  />
                </div>
                <div>
                  <label className="block text-sm font-medium text-gray-700" htmlFor="note">{t("Note")}</label>
                  <textarea
                    id="note"
                    name="note"
                    required
                    className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                  ></textarea>
                </div>
                <div>
                  <label className="block text-sm font-medium text-gray-700" htmlFor="cv">{t("CV")}</label>
                  <input
                    type="file"
                    id="cv"
                    name="cv"
                    required
                    className="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"
                  />
                </div>
                <button
                  type="submit"
                //   className="w-full inline-flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                >
                  {t("Submit")}
                </button>
              </form>
            )}
          </div>
        </>
      );
    }
    
    export const getStaticPaths: GetStaticPaths = async () => {
      const drupalClient = new DrupalClient(env.NEXT_PUBLIC_DRUPAL_BASE_URL, {
        auth: {
          clientId: env.DRUPAL_CLIENT_ID,
          clientSecret: env.DRUPAL_CLIENT_SECRET,
        },
      });
    
      const careersResponse = await drupalClient.getResourceCollection<DrupalNode[]>(
        "node--career_position",
        {
          params: {
            "fields[node--career_position]": "id",
            "page[limit]": 50,
          },
        },
      );
    
      const paths = careersResponse.map((career) => ({
        params: { id: career.id },
      }));
    
      return {
        paths,
        fallback: "blocking",
      };
    };
    
    export const getStaticProps: GetStaticProps<careersProps> = async (
      context,
    ) => {
      const { id } = context.params as { id: string };
      const drupalClient = new DrupalClient(env.NEXT_PUBLIC_DRUPAL_BASE_URL, {
        auth: {
          clientId: env.DRUPAL_CLIENT_ID,
          clientSecret: env.DRUPAL_CLIENT_SECRET,
        },
      });
    
      try {
        const career = await drupalClient.getResource<DrupalNode>(
          "node--career_position",
          id,
        );
    
        return {
          props: {
            ...(await getCommonPageProps(context)),
            career,
          },
          revalidate: 60,
        };
      } catch (error) {
        console.error("Error fetching career post:", error);
        return {
          notFound: true,
        };
      }
    };
    
  • ๐Ÿ‡ง๐Ÿ‡พBelarus andreylugovtsov

    mime_type.guesser service not found, Drupal 10.3.5

Production build 0.71.5 2024