0

I have implemented a feature for PDF conversion using an external java program based on iText (performance is not an issue, the system is for internal usage only, users are less than a hundred, concurrent connections less than ten and daily PDF conversions just a handfull or two). I get the HTML from an element on my page, I append all CSS rules and send the standalone HTML so obtained to the server using a dynamically generated form:

$('#convert-to-pdf-link').click(function() {
    let inner = '<html><head><title>' + document.title  + '</title>' ;

    let rules_text = '' ;
    [...document.styleSheets].forEach(function(sheet) {
        let rules = [ ... (sheet.cssRules || sheet.rules) ] ;
        rules.forEach(function(rule) {
            rules_text += rule.cssText + "\n" ;
        }) ;
    }) ;

    inner += `<style>${rules_text}</style>` ;

    inner += '</head><body >' ;
    let contents = $('.node--type-mytype').html() ;
    inner += contents ;
    inner += '</body></html>' ;

    let form = document.querySelector('form[action="/ls/topdf"]') ;
    if (form === null) {
        form = document.createElement('form') ;
        form.setAttribute('action', '/ls/topdf') ;
        form.setAttribute('method', 'post') ;
        document.body.append(form) ;
    }
    let input_el = form.querySelector('input[name="input_file"]') ;
    if (input_el === null) {
        input_el = document.createElement('input') ;
        input_el.setAttribute('type', 'text') ;
        input_el.setAttribute('name', 'input_file') ;
        input_el.classList.add('hidden') ;
        form.append(input_el) ;
    }
    input_el.value = inner ;
    form.submit() ;

    return false ;
}) ;

The controller gets the html from the form, saves it to file, runs the conversion program, and returns the resulting PDF in a BinaryFileResponse:

class UtilitiesController extends ControllerBase {
 [...]
    public function convertToPdf(Request $request) {
        $scheme = \Drupal::config('system.file')->get('default_scheme') ;
        $dirname = "$scheme://appoggio" ;
        $filesystem = \Drupal::service('file_system') ;
        $filesystem->prepareDirectory($dirname, 
            FileSystemInterface::CREATE_DIRECTORY|FileSystemInterface::CREATE_DIRECTORY) ;
        $basename = time() ;
        $input_uri = "$dirname/$basename.html" ;
        $body = $request->request->get('input_file') ;
        $infile = file_save_data($body, $input_uri, $replace=FileSystemInterface::EXISTS_REPLACE) ;
        $inpath = $filesystem->realpath($input_uri) ;
        $pdf_uri =  "$dirname/$basename.pdf" ;
        $pdfpath = $filesystem->realpath($pdf_uri) ;
        $command = "java -jar /opt/TestConvHtml2Pdf_jar/TestConvHtml2Pdf.jar '$inpath' '$pdfpath'" ;
        exec($command, $output, $retval) ;
        if ($retval === 0) {
            $response = new BinaryFileResponse($pdf_uri, 200, ['Content-Type' => 'application/pdf']) ;
            $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, "$basename.pdf") ;
            return $response ;
        }
        else {
            $this->log->error("odg_to_pdf() failed with error code $retval. Details:<br>" . 
                implode('<br>', $output)) ;
            foreach ($output as $line)
                $rows[] = [ $line ] ;
            return [
                '#theme' => 'table',
                '#header' => [ "Errore nella conversione in PDF: $retval" ],
                '#rows' => $rows,
            ] ;
        }
    }
}

Everything works nicely, but only if I keep the route to the controller open, i.e., in mymodule.routing.yml,

mymodule.to_pdf:
  path: '/ls/topdf'
  defaults:
      _controller: '\Drupal\mymodule\Controller\UtilitiesController::convertToPdf'
  methods: [POST]
  options:
    no_cache: 'TRUE'
  requirements:
    _access: 'TRUE'

If I replace _access: 'TRUE' with _role: 'authenticated', I get a 403, in spite of the fact that the session cookie is present in the request. I tried to spot other differences in the headers of the request for conversion and of other requests, but I couldn't find any.

What am I missing?

5
  • 1
    POST requests need a CSRF token, are you passing that?
    – Lambic
    Jul 18, 2022 at 13:19
  • 1
    Maybe try with a _permission rather than a _role.
    – Jaypan
    Jul 19, 2022 at 5:47
  • 1
    If what @Jaypan suggested does not work, I would suggest _user_is_logged_in: 'TRUE' under requirements
    – pixel5
    Jul 19, 2022 at 15:25
  • @Jaypan, I came late to your simple suggestione – and it worked! If you add an answer I'll give you the bounty reward. Could you also please explain why the _permission worked while the _role didn't? Jul 22, 2022 at 8:18
  • I have added a response.
    – Jaypan
    Jul 22, 2022 at 13:44

2 Answers 2

2
+50

Roles are config, whereas routes are code. As such, it's generally better to set a route to a permission, which is code based, rather than a role, so that both the route and permission are controlled in code. As such, try changing _role to a _permission, and set that permission to the given role.

1
  • As I said above, using a _permission rather than a _role solved my problem – many thanks! Jul 25, 2022 at 9:45
1

Try adding _csrf_token: "TRUE" in the routing requirements, like so,

requirements:
_csrf_token: 'TRUE'

You will also be required to auto generate the token from Drupal and use it in the request uri like so /ls/topdf?token=some auto generated token

Here is an example of how to auto generate a token from Drupal (I'm not sure if your Javascript runs on Java or on The Drupal app, but assuming it's from the Drupal app, you can pass the token from Drupal to your js);

$url = \Drupal\Core\Url::fromRoute('mymodule.to_pdf');
$token_service = Drupal::service('csrf_token')
$the_token = $token_service->get($url->getInternalPath());

Thus:

form.setAttribute('action', `/ls/topdf?token=${the_token from drupalSettings}`) ;

Another solution might be using an absolute path in the form action, using your current setup without changing anything about it.

And another would be to include an options key in your route definitions with parameters of an entity type user.

options:
  parameters:
    user:
      type: entity:user

`// update your controller method`
public function convertToPdf(AccountInterface $user, Request $request) {

and using that user entity to check for, is authenticated, has permissions and all, as your application requires.

2
  • Well, the second solution is not viable because the path in my form is already absolute – unless you mean I should insert also protocol and hostname, and I can't easily do that due to different environments. I tried the third since it looked simplest, but if I do what you wrote I don't get an username in my controller – maybe some piece is missing? So I tried the first – I managed to pass the csrf token to my controller, but I still get a 403 on my POST request. So I keep being stuck... Jul 22, 2022 at 8:04
  • 1
    Your answer was very interesting, but I had to award the bounty (a small one, I don't have a lot of reputation...) to Jaypan, since their solution solved my problem very seimply. Thank for all the suggestions, though! Jul 22, 2022 at 15:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.