Saturday 9 October 2021

Finding website paths for Drupal modules

As part of my side projects, I have been helping a friend move his Drupal 8 website from a proof of concept towards going live. One of the problems I was facing was how modules map to URLs so I wrote a little Perl script to generate a URL tree from module routing files.

tree_routes.pl
#!/usr/bin/perl

use strict;
use warnings;
use diagnostics;

use YAML::Tiny;
use Data::Dumper;
use Tree::DAG_Node;

# Nice Tree
{
   package NiceTree;
   use Tree::DAG_Node;
   our @ISA = qw(Tree::DAG_Node);

   sub new {
      my $class = shift;
      my $options = shift;
      my $self = bless $class->SUPER::new($options);
      return $self;
   }

   sub hashref2string {
      my ($self, $hashref) = @_;
      $hashref ||= {};
      return join(", ", map {qq|$_ => "$$hashref{$_}"|} sort keys %$hashref);
   }

   sub format_node {
      my ($self, $options, $node) = @_;
      my ($s) = $node->name;

      if (keys %{ $node->attributes } > 0) {
         $s .= "\t" . $self->hashref2string($node->attributes) if (!$$options{no_attributes});
      }

      return $s;
   }

   sub node2string {
      my ($self, $options, $node, $vert_dashes) = @_;
      my ($depth) = scalar($node->ancestors) || 0;
      my ($sibling_count) = defined $node->mother ? scalar $node->self_and_sisters : 1;
      my ($offset) = ' ' x 3;
      my (@indent) = map {$$vert_dashes[$_] || $offset} 0 .. $depth - 1;
      @$vert_dashes =
         (
            @indent,
            ($sibling_count == 1 ? $offset : '  |'),
         );

      if ($sibling_count == ($node->my_daughter_index + 1)) {
         $$vert_dashes[$depth] = $offset;
      }

      my $x = ((scalar($node->daughters) || 0) ? '  |--+ ' : '  |--- ');

      return join('' => @indent[1 .. $#indent]) . ($depth ? $x : '') . $self->format_node($options, $node);

   }
}

# Find files in path
{
   my @paths;
   sub dir_listing {
      my ($root) = @_;
      $root .= '/' unless $root =~ /\/$/;
      for my $f (glob "$root*") {
         push @paths, $f;
         dir_listing($f) if -d $f;
      }
      return @paths;
   }
}

# Generate tree of URLs
{
   my $path_tree = NiceTree->new({ name => '/' });
   # my $path_tree = Tree::DAG_Node->new({ name => '/' });

   sub add_node {
      my ($current_node, $leaf) = @_[0, 1];

      my $new_daughter = $current_node->new_daughter({ name => $leaf });

      # Keep daughters in alphabetical order
      $current_node->set_daughters(sort {$a->name cmp $b->name} $current_node->daughters);

      return $new_daughter;
   }

   sub get_node_for_path {
      my @paths = split '/', $_[0];

      my $current_node = $path_tree; # Start at the root

      foreach my $leaf (@paths) {
         if ($leaf ne '') {
            my $found = 0;
            my @daughters = $current_node->daughters();

            if (@daughters) {
               foreach my $dau (@daughters) {
                  if ($dau->name eq $leaf) {
                     $current_node = $dau;
                     $found = 1;
                     last;
                  }
               }
            }

            if (not $found) {
               $current_node = add_node($current_node, $leaf);
            }
         }
      }

      return $current_node;
   }
}


sub process_requirements {
   my %requirements = @_;
   my $path_node = $requirements{'path_node'};

   if (defined $requirements{'_permission'}) {
      $path_node->attributes->{'Permission'} = $requirements{'_permission'};
   }

   if (defined $requirements{'_entity_access'}) {
      $path_node->attributes->{'Access'} = $requirements{'_entity_access'};
   }
}


sub process_defaults {
   my %defaults = @_;

   my $path_node = $defaults{'path_node'};

   if (defined $defaults{'_title'}) {
      $path_node->attributes->{Title} = $defaults{'_title'};
   }

   if (defined $defaults{'_controller'}) {
      $path_node->attributes->{'Controller'} = $defaults{'_controller'};
   }

   if (defined $defaults{'_entity_form'}) {
      $path_node->attributes->{'Entity_form'} = $defaults{'_entity_form'};
   }

   if (defined $defaults{'_entity_list'}) {
      $path_node->attributes->{'Entity_list'} = $defaults{'_entity_list'};
   }
}


sub process_form {
   my %form = @_;

   my $filename = $form{'filename'};
   my $formname = $form{'formname'};
   my $path = $form{'path'};

   my $node = get_node_for_path($path);

   $node->attributes->{'Form'} .= $formname . ";";
   $node->attributes->{'File'} .= $filename . ";";

   process_defaults(%{$form{'defaults'}}, path_node => $node);
   process_requirements(%{$form{'requirements'}}, path_node => $node);
}


sub process_yml_file {
   my $filename = $_[0];
   my $yaml = YAML::Tiny->read($filename);

   # print "File: @_\n";
   if (defined $yaml->[0]) {

      my %k = %{$yaml->[0]};
      # print Dumper(%k);

      for my $form (keys %k) {
         if ($form ne '') {
            if (ref($k{$form}) eq 'HASH') {
               process_form(%{$k{$form}}, filename => $filename, formname => $form);
            }
            else {
               warn("Form: $form in file $filename is " . ref($k{$form}) . "\n");
            }
         }
      }
   }
   else {
      warn("yaml file $filename is empty\n");
   }
}


my @paths = dir_listing(@ARGV);

foreach my $file (@paths) {
   if ($file =~ /\.routing\.yml$/) {
      eval {
         process_yml_file($file)
      };
      if ($@ ne '') {
         warn("Error caught while processing file $file\n");
         warn(@$);
      }

   }
}

my $tree = get_node_for_path('/');
print "Tree is:\n";
print map("$_\n", @{$tree->tree2string});

Drupal 8 custom module permissions

If like me you have been wondering how the Drupal 8/9 custom module yml files link together, it goes something like this:
xxxx.routing.yml
free_resources_dashboard:
   path: '/free-resources'
   defaults:
      _controller: '\Drupal\free_resources\Controller\FreeResources::dashboard'
      _title: 'My Resources'
   requirements:
      _permission: 'free resources dashboard'
xxxx.permissions.yml
free resources dashboard:
   title: 'Free Resources Dashboard'
   description: 'Allow access to the free resources dashboard page'

The free resources dashboard: line in the xxxx.permissions.yml file links to the _permissions: 'free resources dashboard' line in the xxxx.routing.yml file. If the _permissions: field does not match any of the defined permissions (for the site?) you will get a permissions error when you try and access your page.