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.

Tuesday 6 April 2021

Sharing printers via AirPrint

I had been struggling to get my old Canon MP560 to work with my new Mac Mini running Bir Sur. I decided to use my Raspberry Pi 4 running Ubuntu Server 20.04 as a printer server and at the same time to see if I could finally get printing to work from my iPhone.

I discovered that it is relatively straight forward to setup printer sharing via AirPrint. I based my inital attempt on the Linux Magazine's atricle on setting up AirPrint but I have different settings for the cups configuration and didn't need to add the mime types. I also needed to adjust the firewall settings and get my printers working from Linux as I had not previously set them up for Linux, only for Windows and MacOS

CUPS installation

For the CUPS installation I used the following packages:

  • cups-bsd
  • avahi
  • printer-driver-gutenprint

I installed CUPS and the Gutenprint printer drivers (which include drivers for both my Canon MP560 and HP Laserjet 6P) using the following steps:

sudo apt-get update sudo apt-get install cups-bsd avahi-daemon printer-driver-gutenprint sudo systemctl start cups sudo systemctl enable cups

I then edited the CUPS configuration and changed only the bits shown below. I left the rest of the file alone.

# Only listen for connections from the local machine. Port 631 Listen /run/cups/cups.sock # Show shared printers on the local network. Browsing On BrowseLocalProtocols dnssd # Default authentication type, when authentication is required... DefaultAuthType Basic # Web interface setting... WebInterface Yes # Restrict access to the server... <Location /> Order allow,deny Allow @LOCAL </Location> # Restrict access to the admin pages... <Location /admin> AuthType Default Require valid-user Order allow,deny Allow @LOCAL </Location> # Restrict access to configuration files... <Location /admin/conf> AuthType Default Require user @SYSTEM Order allow,deny </Location>

I then configured the firewall to allow access to CUPS and Avahi. Note the network this setup is on is 192.168.200.0 as seen in the config below - change as required.

sudo ufw allow in from 192.168.200.0/24 to any port 631 sudo ufw allow 5353/udp

I then added any required users to the lpadmin group. eg sudo adduser XXX lpadmin before restarting CUPS (sudo systemctl restart cups) and starting and enabling avahi-daemon.

sudo systemctl start avahi-daemon sudo systemctl enable avahi-daemon sudo systemctl restart cups

I then connected to my Raspberry Pi on port 631 and used the CUPS admin interface to add my Canon MP560 and HP Laser 6P printers and checked that they worked by printing a test page for each.

AirPrint configuration

Once I had confirmed that the printers were setup and that the Raspberry Pi could successfully print from them, I used the 'airprint-generate.py' script from https://github.com/tjfontaine/airprint-generate/archive/refs/heads/master.zip to generate avahi service files. 'airprint-generate.py' has the following dependencies:

  • python3
  • python3-cups
sudo apt-get install python3 python3-cups unzip wget https://github.com/tjfontaine/airprint-generate/archive/refs/heads/master.zip unzip master.zip cd airprint-generate-master/ ./airprint-generate.py

Both of my printers were now visible to and usable from both my iPhone and Mac Mini.

Thursday 14 January 2021

File name wildcards

Earlier I was working on a command line utility that needed to take a list of potentially wildcarded files as input. I didn't want a fully blown regular expression as the input, just the normal ? and * options.

I decided that the quickest way to do this was to convert the wildcarded filename to a regular expression and then use the STL std::regex to do the matching.

Here is a quick way to do it.

#pragma once #include <regex> #include <string> inline std::regex convert_file_wildcards_to_regex(std::string file_pattern) { std::string pattern; for (auto c : file_pattern) { switch (c) { default: pattern += c; break; case '.': pattern += "\\."; break; case '\\': pattern += "\\\\"; break; case '?': pattern += "."; break; case '*': pattern += ".*"; break; } } return std::regex(pattern, std::regex_constants::ECMAScript | std::regex_constants::icase); }