[LON-CAPA-cvs] cvs: doc /loncapafiles axe_config_check.piml loncapafiles.lpml loncom loncapa_apache.conf loncom/build Makefile loncom/html/adm/help/tex Docs_Accessibility_Check.tex loncom/html/res/adm/pages accessibility.png loncom/interface loncommon.pm loncourserespicker.pm londocs.pm loncom/node.js/axe AxeRunner.pm axeperl.js lonaxe.pl package.json

raeburn raeburn at source.lon-capa.org
Wed Dec 31 18:44:31 EST 2025


raeburn		Wed Dec 31 23:44:31 2025 EDT

  Added files:                 
    /loncom/node.js/axe	axeperl.js package.json AxeRunner.pm lonaxe.pl 
    /loncom/html/res/adm/pages	accessibility.png 
    /loncom/html/adm/help/tex	Docs_Accessibility_Check.tex 
    /doc/loncapafiles	axe_config_check.piml 

  Modified files:              
    /loncom/interface	londocs.pm loncourserespicker.pm loncommon.pm 
    /loncom/build	Makefile 
    /loncom	loncapa_apache.conf 
    /doc/loncapafiles	loncapafiles.lpml 
  Log:
  - Accessibility testing using axe-core and puppeteer (node.js) using a 
    headless chromium web browser. Dependencies on nodejs and chromium in
    LONCAPA-prerequisites 1.36. 
  
  
-------------- next part --------------
Index: loncom/interface/londocs.pm
diff -u loncom/interface/londocs.pm:1.734 loncom/interface/londocs.pm:1.735
--- loncom/interface/londocs.pm:1.734	Sat Dec 27 22:26:00 2025
+++ loncom/interface/londocs.pm	Wed Dec 31 23:44:27 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # Documents
 #
-# $Id: londocs.pm,v 1.734 2025/12/27 22:26:00 raeburn Exp $
+# $Id: londocs.pm,v 1.735 2025/12/31 23:44:27 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -6279,6 +6279,100 @@
     $r->print(&endContentScreen());
 }
 
+sub wcag_check_form {
+    my ($r,$canedit,$cnum,$cdom) = @_;
+    my $crstype = &Apache::loncommon::course_type();
+    my $formname = 'wcagcheck';
+    $r->print(&Apache::loncommon::start_page('Choose Resources for Accessibility checking'));
+    $r->print(&Apache::lonhtmlcommon::breadcrumbs('Accessibility'));
+    $r->print(&startContentScreen('tools'));
+    my ($navmap,$errormsg) =
+        &Apache::loncourserespicker::get_navmap_object($crstype,'wcag');
+    my (%maps,%resources,%titles);
+    if (!ref($navmap)) {
+        $r->print($errormsg);
+    } else {
+        $r->print('<h2 class="LC_heading_2">'.&mt('Resources to check for accessibility').'</h2>'."\n");
+        $r->rflush();
+        my $readonly;
+        if ($canedit) {
+
+        } else {
+            $readonly = 1;
+        }
+        $r->print(&Apache::loncourserespicker::create_picker($navmap,'wcagcheck',$formname,$crstype,undef,
+                                                             undef,undef,undef,undef,undef,undef,undef,$readonly));
+    }
+    $r->print(&endContentScreen());
+}
+
+sub wcag_check_results {
+    my ($r,$canedit,$cnum,$cdom,$checkmain,$checksupp) = @_;
+    my $crstype = &Apache::loncommon::course_type();
+    my $formname = 'wcagcheck';
+    my $filelist;
+    my ($nummain,$numsupp) = (0,0);
+    $r->print(&Apache::loncommon::start_page('Accessibility Results'));
+    $r->print(&Apache::lonhtmlcommon::breadcrumbs('Accessibility'));
+    $r->print(&startContentScreen('tools'));
+    if ((ref($checkmain) eq 'ARRAY') && (ref($checksupp) eq 'ARRAY')) {
+        if (@{$checkmain} > 0) {
+            my ($navmap,$errormsg) =
+                &Apache::loncourserespicker::get_navmap_object($crstype,'wcag');
+            my (%maps,%resources,%titles);
+            if (!ref($navmap)) {
+                $r->print($errormsg);
+            } else {
+                &Apache::loncourserespicker::enumerate_course_contents($navmap,\%maps,\%resources,\%titles,
+                                                                       'wcagcheck',$cdom,$cnum);
+                foreach my $item (@{$checkmain}) {
+                    if (exists($resources{$item})) {
+                        $nummain ++;
+                        $filelist .= "M\0$resources{$item}\0$titles{$item}\n";
+                    }
+                }
+            }
+        }
+        if (@{$checksupp} > 0) {
+            my $suppcount = 0;
+            my $mapnum = 0;
+            my (%suppmaps,%suppresources,%supptitles);
+            &Apache::loncourserespicker::enumerate_supp_contents('wcagcheck',$cnum,$cdom,$mapnum,\$suppcount,
+                                                                 \%suppmaps,\%suppresources,\%supptitles);
+            foreach my $item (@{$checksupp}) {
+                if (exists($suppresources{$item})) {
+                    $numsupp ++;
+                    $filelist .= "S\0$suppresources{$item}\0$supptitles{$item}\n";
+                }
+            }
+        }
+    }
+    if ($filelist) {
+        my $identifier = &Apache::loncommon::get_cgi_id();
+        my $filename = "/home/httpd/axespool/$env{'user.name'}_$env{'user.domain'}_axe_$identifier.txt";
+        if (open(my $fh,'>',$filename)) {
+            print $fh $filelist;
+            close($fh);
+            &Apache::lonnet::appenv({'cgi.'.$identifier.'.file' => $filename,
+                                     'cgi.'.$identifier.'.user' => $env{'user.name'},
+                                     'cgi.'.$identifier.'.domain' => $env{'user.domain'},
+                                     'cgi.'.$identifier.'.cnum' => $cnum,
+                                     'cgi.'.$identifier.'.cdom' => $cdom,
+                                     'cgi.'.$identifier.'.main' => $nummain,
+                                     'cgi.'.$identifier.'.supp' => $numsupp,
+                                     'cgi.'.$identifier.'.crstype' => $crstype});
+        }
+        my $continue_text = &mt('Continue');
+        $r->print(<<END);
+<br />
+<meta http-equiv="Refresh" content="0; url=/cgi-bin/lonaxe.pl?$identifier" />
+<a href="/cgi-bin/lonaxe.pl?$identifier">$continue_text</a>
+END
+    }
+    $r->print(&endContentScreen());
+    return;
+}
+
 sub contentverifyform {
     my ($r) = @_;
     my $crstype = &Apache::loncommon::course_type();
@@ -6855,6 +6949,15 @@
   } elsif ($canedit && $env{'form.exportcourse'}) {
       &init_breadcrumbs('exportcourse','IMS Export');
       &Apache::imsexport::exportcourse($r);
+  } elsif ($allowed && $env{'form.wcagcheck'}) {
+      &init_breadcrumbs('wcagcheck','Select Resources for Accessibility Check','Docs_Accessibility_Check');
+      my @checkmain = &Apache::loncommon::get_env_multiple('form.wcagmain');
+      my @checksupp = &Apache::loncommon::get_env_multiple('form.wcagsupp');
+      if ((scalar(@checkmain) > 0) || (scalar(@checksupp) > 0)) {
+          &wcag_check_results($r,$canedit,$coursenum,$coursedom,\@checkmain,\@checksupp);
+      } else {
+          &wcag_check_form($r,$canedit,$coursenum,$coursedom);
+      }
   } else {
       if ($canedit && $env{'form.authorrole'}) {
           $noendpage = 1;
@@ -8510,6 +8613,7 @@
                                          'imse' => 'Export contents to IMS Archive',
                                          'dcd'  => 'Copy uploaded content to Authoring Space',
                                          'cpc'  => 'Copy from Course Authoring to User Authoring',
+                                         'cfa'  => 'Check for Accessibility',
             );
     my ($candump,$dumpurl,$exportcrsurl);
     if ($home + $other > 0) {
@@ -8572,6 +8676,14 @@
                     alttext    => '',
                     linktitle  => "Set shortened URLs for a resource or folder in your $lc_crstype for use in deep-linking"
                 },
+                {   linktext   => $lt{'cfa'},
+                    url        => "javascript:injectData(document.courseverify,'dummy','wcagcheck','$lt{'cfa'}')",
+                    permission => 'F',
+                    help       => 'Docs_Accessibility_Check',
+                    icon       => 'accessibility.png',
+                    alttext    => '',
+                    linktitle  => "Accessibility checking for selected resources in your $lc_crstype"
+                },
                 ]
         });
     if ($canedit) {
Index: loncom/interface/loncourserespicker.pm
diff -u loncom/interface/loncourserespicker.pm:1.24 loncom/interface/loncourserespicker.pm:1.25
--- loncom/interface/loncourserespicker.pm:1.24	Fri Dec 19 21:55:48 2025
+++ loncom/interface/loncourserespicker.pm	Wed Dec 31 23:44:27 2025
@@ -1,6 +1,6 @@
 # The LearningOnline Network
 #
-# $Id: loncourserespicker.pm,v 1.24 2025/12/19 21:55:48 raeburn Exp $
+# $Id: loncourserespicker.pm,v 1.25 2025/12/31 23:44:27 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -268,6 +268,8 @@
         $chkname = 'addtiny';
     } elsif ($context eq 'passback') {
         $chkname = 'passback';
+    } elsif ($context eq 'wcagcheck') {
+        $chkname = 'wcagmain';
     }
     $it = $navmap->getIterator(undef,undef,undef,1,undef,undef);
     if (ref($blockedmaps) eq 'HASH') {
@@ -335,6 +337,9 @@
     my %parent = ();
     my %children = ();
     my %hierarchy = ();
+    my %supp_parent = ();
+    my %supp_children = ();
+    my %supp_hierarchy = ();
     my $location=&Apache::loncommon::lonhttpdurl("/adm/lonIcons");
     my $whitespace =
         '<img src="'.$location.'/whitespace_21.gif" class="LC_docs_spacer" alt="" />';
@@ -375,6 +380,8 @@
         if ($readonly) {
             $disabled = ' disabled="disabled"';
         }
+    } elsif ($context eq 'wcagcheck') {
+        $startcount = 2;
     }
     if ($disabled) {
         $togglebuttons = '<br />';
@@ -405,10 +412,15 @@
                 '</fieldset>';
         }
         $display .= '</div>';
-    } elsif ($context eq 'shorturls') {
+    } elsif (($context eq 'shorturls') || ($context eq 'wcagcheck')) {
+        my $legend = $togglebuttons;
+        if ($context eq 'wcagcheck') {
+            $legend = '<span class="LC_nobreak">'.&mt('Main Content').': '.$togglebuttons.'</span>';
+            $display .= '<div class="LC_floatleft">';
+        }
         unless ($disabled) {
             $display .= '<fieldset style="display: inline">'.
-                        '<legend>'.$togglebuttons.'</legend>'."\n";
+                        '<legend>'.$legend.'</legend>'."\n";
         }
     } elsif (($context eq 'examblock') || ($context eq 'passback')) {
         $display .= $info.$togglebuttons;
@@ -437,11 +449,13 @@
     } elsif ($context eq 'shorturls') {
         $display .= '<th colspan="2">'.&mt('Tiny URL').'</th>'.
                     '<th>'.&mt("Title in $crstype").'</th>';
+    } elsif ($context eq 'wcagcheck') {
+        $display .= '<th>'.&mt('Check for Accessibility?').'</th>';
     } elsif ($context eq 'passback') {
         $display .= '<th>'.&mt("Title in $crstype").'</th>'.
                     '<th>'.&mt('Tiny URL Deep-link').'</th>'.
                     '<th>'.&mt('Launcher').'</th>'.
-                    '<th  style="padding-left: 6px; padding-right: 6px">'.&mt('Score Type').'</th>'.
+                    '<th style="padding-left: 6px; padding-right: 6px">'.&mt('Score Type').'</th>'.
                     '<th style="padding-left: 6px; padding-right: 6px">'.&mt('Select').'</th>';
     }
     $display .= &Apache::loncommon::end_data_table_header_row();
@@ -538,7 +552,7 @@
             }
             $showitem .= '<img '.$icon.' /> '."\n";
             $children{$parent{$depth}} .= $currelem.':';
-            if ($context eq 'examblock') {
+            if (($context eq 'examblock') || ($context eq 'wcagcheck')) {
                 if ($parent{$depth} > 1) {
                     if ($hierarchy{$parent{$depth}}) {
                         $hierarchy{$currelem} = $hierarchy{$parent{$depth}}.",'$parent{$depth}'";
@@ -693,6 +707,46 @@
                 '<input type="submit" name="shorturls" value="'.
                 &mt('Create Tiny URL(s)').'" /></p></fieldset>';
         }
+    } elsif ($context eq 'wcagcheck') {
+        my $suppcount = 0;
+        unless ($readonly) {
+            $display .= '</fieldset></div>';
+            my $supp;
+            my ($rescount,$supptitles,$ordered,$src_by_id) = &get_supp_hashes($cnum,$cdom);
+            my ($supplemental) = &Apache::loncommon::get_supplemental($cnum,$cdom,1);
+            if ($rescount) {
+                my $otherchk = 'wcagsupp';
+                $supp = '<div class="LC_floatleft">'.
+                        '<fieldset><legend><span class="LC_nobreak">'.
+                        &mt('Supplemental Content').': '.
+                        '<input type="button" value="'.&mt('check all').'" '.
+                        'onclick="javascript:checkAll(document.'.$formname.'.'.$otherchk.')" />'.
+                        '  <input type="button" value="'.&mt('uncheck all').'"'.
+                        ' onclick="javascript:uncheckAll(document.'.$formname.'.'.$otherchk.')" />'.
+                        '</span></legend>'.
+                        &Apache::loncommon::start_data_table().
+                        &Apache::loncommon::start_data_table_header_row().
+                        '<th>'.&mt('Check for Accessibility?').'</th>'.
+                        &Apache::loncommon::end_data_table_header_row();
+                        my $mapnum = 0;
+                        my $depth = 1;
+                        my $startsupp = $startcount + $count + 3;
+                        $lastcontainer = $startsupp;
+                        $supp_parent{$depth} = $lastcontainer;
+                        $supp .= &recurse_supp($formname,$startsupp,$depth,$mapnum,$supptitles,$ordered,
+                                               $src_by_id,\$suppcount,\%supp_parent,\%supp_children,
+                                               \%supp_hierarchy,\$lastcontainer).
+                        &Apache::loncommon::end_data_table();
+            }
+            if ($supp) {
+                $supp .= '</fieldset></div>';
+            }
+            $display .= $supp.'<div style="padding:0;clear:both;margin:0;border:0"></div>'.
+                        '<div>'.
+                        '<input type="submit" name="wcagcheck" value="'.&mt('Check Accessibility').'" />'.
+                        '</div>';
+        }
+        $numcount = $count + $startcount + $suppcount + 3;
     } elsif ($context eq 'passback') {
         unless ($readonly) {
             $display .=
@@ -709,8 +763,9 @@
     }
     my $scripttag =
         &respicker_javascript($startcount,$numcount,$context,$formname,\%children,
-                              \%hierarchy,\@checked_maps,$numhome,$chkname);
-    if (($context eq 'dumpdocs') || ($context eq 'shorturls')) {
+                              \%hierarchy,\@checked_maps,$numhome,$chkname,
+                              \%supp_children,\%supp_hierarchy);
+    if (($context eq 'dumpdocs') || ($context eq 'shorturls') || ($context eq 'wcagcheck')) {
         return $scripttag.$display; 
     }
     my ($title,$crumbs,$args);
@@ -739,9 +794,74 @@
     return $output;
 }
 
+sub recurse_supp {
+    my ($formname,$startcount,$depth,$mapnum,$supptitles,$ordered,$src_by_id,
+        $countref,$parent,$children,$hierarchy,$lastcontainer) = @_;
+    my $output = '';
+    my $chkname = 'wcagsupp';
+    my $location = &Apache::loncommon::lonhttpdurl("/adm/lonIcons");
+    if ((ref($supptitles) eq 'HASH') && (ref($ordered) eq 'HASH') &&
+        (ref($src_by_id) eq 'HASH')) {
+        my $deeper = $depth + 1;
+        my $shallower = $depth - 1;
+        if (ref($ordered->{$mapnum}) eq 'ARRAY') {
+            my ($showitem,$whitespace);
+            $whitespace =
+                '<img src="'.$location.'/whitespace_21.gif" class="LC_docs_spacer" alt="" />';
+            for (my $i=0; $i<$depth; $i++) {
+                $showitem .= "$whitespace\n";
+            }
+            foreach my $idx (@{$ordered->{$mapnum}}) {
+                $$countref ++;
+                my ($currelem,$is_map,$newmap);
+                $currelem = $$countref+$startcount;
+                $children->{$parent->{$depth}} .= $currelem.':';
+                if ($parent->{$depth} > 1) {
+                    if ($hierarchy->{$parent->{$depth}}) {
+                        $hierarchy->{$currelem} = $hierarchy->{$parent->{$depth}}.",'".$parent->{$depth}."'";
+                    } else {
+                        $hierarchy->{$currelem} = "'".$parent->{$depth}."'";
+                    }
+                }
+                $output .= &Apache::loncommon::start_data_table_row()."\n";
+                if ($src_by_id->{$mapnum.':'.$idx} =~ /supplemental_(\d+)\.sequence$/) {
+                    $newmap = $1;
+                    $is_map = 1;
+                    $$lastcontainer = $currelem;
+                }
+                my $icon = 'src="'.$location.'/unknown.gif" alt=""';
+                if ($is_map) {
+                    $icon = 'src="'.$location.'/navmap.folder.open.gif" alt="'.&mt('Folder').'"';
+                } elsif ($src_by_id->{$mapnum.':'.$idx} ne '') {
+                    $icon = 'src="'.&Apache::loncommon::icon($src_by_id->{$mapnum.':'.$idx}).'" alt=""';
+                }
+                my $labeltext = ' aria-label="'.&mt('copy item').'"';
+                $output .= '<td><input type="checkbox" name="'.$chkname.'" value="'.$$countref.'" ';
+                if ($is_map) {
+                    $output .= 'onclick="javascript:checkFolder(document.'.$formname.','."'$currelem'".')" ';
+                } else {
+                    $output .= 'onclick="javascript:checkResource(document.'.$formname.','."'$currelem'".')" ';
+                }
+                $output .= $labeltext.' />'."\n";
+                $output .= $showitem.'<img '.$icon.' /> '."\n".
+                           ' '.$supptitles->{"$mapnum:$idx"}.$whitespace.'</td>';
+                $output .= &Apache::loncommon::end_data_table_row()."\n";
+                if ($is_map) {
+                    $parent->{$deeper} = $$lastcontainer;
+                    $output .= &recurse_supp($formname,$startcount,$deeper,$newmap,$supptitles,
+                                             $ordered,$src_by_id,$countref,$parent,$children,
+                                             $hierarchy,$lastcontainer);
+                }
+            }
+            $$lastcontainer = $parent->{$shallower};
+        }
+    }
+    return $output;
+}
+
 sub respicker_javascript {
     my ($startcount,$numitems,$context,$formname,$children,$hierarchy,
-        $checked_maps,$numhome,$chkname) = @_;
+        $checked_maps,$numhome,$chkname,$supp_children,$supp_hierarchy) = @_;
     my $check_uncheck = <<"FIRST";
 function checkAll(field) {
     if (field.length > 0) {
@@ -822,16 +942,30 @@
         }
     }
 
-    if ($context eq 'examblock') {
+    if (($context eq 'examblock') || ($context eq 'wcagcheck')) {
+        if ($context eq 'wcagcheck') {
+            if ((ref($supp_children) eq 'HASH') && (ref($supp_hierarchy) eq 'HASH')) {
+                foreach my $container (sort { $a <=> $b } (keys(%{$supp_children}))) {
+                    my @suppcontents = split(/:/,$supp_children->{$container});
+                    for (my $i=0; $i<@suppcontents; $i ++) {
+                        $scripttag .= 'parents['.$container.']['.$i.'] = '.$suppcontents[$i]."\n";
+                    }
+                }
+            }
+        }
         foreach my $item (sort { $a <=> $b } (keys(%{$hierarchy}))) {
             $scripttag .= "nesting[$item] = new Array($hierarchy->{$item});\n";
         }
-         
-        my @sorted_maps = sort { $a <=> $b } (@{$checked_maps});
-        for (my $i=0; $i<@sorted_maps; $i++) {
-            $scripttag .= "initial[$i] = '$sorted_maps[$i]'\n";
-        }
-        $scripttag .= <<"EXTRA";
+        if ($context eq 'wcagcheck') {
+            foreach my $item (sort { $a <=> $b } (keys(%{$supp_hierarchy}))) {
+                $scripttag .= "nesting[$item] = new Array($supp_hierarchy->{$item});\n";
+            }
+        } elsif ($context eq 'examblock') {
+            my @sorted_maps = sort { $a <=> $b } (@{$checked_maps});
+            for (my $i=0; $i<@sorted_maps; $i++) {
+                $scripttag .= "initial[$i] = '$sorted_maps[$i]'\n";
+            }
+            $scripttag .= <<"EXTRA";
 
 function recurseFolders() {
     if (initial.length > 0) {
@@ -843,6 +977,7 @@
 }
 
 EXTRA
+        }
     } elsif ($context eq 'dumpdocs') {
         my $blankmsg = &mt('An item selected has no filename set in the "Save as ..." column.');
         my $dupmsg = &mt('Items selected for copying need unique filenames in the "Save as ..." column.');
@@ -1079,4 +1214,57 @@
     return;
 }
 
+sub get_supp_hashes {
+    my ($cnum,$cdom) = @_;
+    my ($src_by_id,$supptitles,$ordered,$rescount);
+    $rescount = 0;
+    my ($supplemental) = &Apache::loncommon::get_supplemental($cnum,$cdom,1);
+    if (ref($supplemental) eq 'HASH') {
+        if ((ref($supplemental->{'ids'}) eq 'HASH')) {
+            if (keys(%{$supplemental->{'ids'}})) {
+                foreach my $src (keys(%{$supplemental->{'ids'}})) {
+                    if (ref($supplemental->{'ids'}->{$src}) eq 'ARRAY') {
+                        foreach my $id (@{$supplemental->{'ids'}->{$src}}) {
+                            $src_by_id->{$id} = $src;
+                            unless ($src =~ /\.sequence$/) {
+                                $rescount ++;
+                            }
+                        }
+                    }
+                }
+                if (ref($supplemental->{'titles'}) eq 'HASH') {
+                    $supptitles = $supplemental->{'titles'};
+                }
+                if (ref($supplemental->{'ordered'}) eq 'HASH') {
+                    $ordered = $supplemental->{'ordered'};
+                }
+            }
+        }
+    }
+    return ($rescount,$supptitles,$ordered,$src_by_id);
+}
+
+sub enumerate_supp_contents {
+    my ($context,$cnum,$cdom,$mapnum,$countref,$map_url,$resource_url,$titleref) = @_;
+    my ($rescount,$supptitles,$ordered,$src_by_id) = &get_supp_hashes($cnum,$cdom);
+    if ((ref($supptitles) eq 'HASH') && (ref($ordered) eq 'HASH') &&
+        (ref($src_by_id) eq 'HASH')) {
+        if (ref($ordered->{$mapnum}) eq 'ARRAY') {
+            my ($is_map,$newmap);
+            foreach my $idx (@{$ordered->{$mapnum}}) {
+                $$countref ++;
+                if ($src_by_id->{$mapnum.':'.$idx} =~ /supplemental_(\d+)\.sequence$/) {
+                    $newmap = $1;
+                    $is_map = 1;
+                    $map_url->{$$countref} = $src_by_id->{$mapnum.':'.$idx};
+                    &enumerate_supp_contents($context,$cnum,$cdom,$newmap,$countref,$map_url,$resource_url,$titleref);
+                } else {
+                    $resource_url->{$$countref} = $src_by_id->{$mapnum.':'.$idx};
+                }
+                $titleref->{$$countref} = $supptitles->{"$mapnum:$idx"};
+            }
+        }
+    }
+}
+
 1;
Index: loncom/interface/loncommon.pm
diff -u loncom/interface/loncommon.pm:1.1495 loncom/interface/loncommon.pm:1.1496
--- loncom/interface/loncommon.pm:1.1495	Wed Dec 31 23:24:16 2025
+++ loncom/interface/loncommon.pm	Wed Dec 31 23:44:27 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # a pile of common routines
 #
-# $Id: loncommon.pm,v 1.1495 2025/12/31 23:24:16 raeburn Exp $
+# $Id: loncommon.pm,v 1.1496 2025/12/31 23:44:27 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -19742,10 +19742,10 @@
             my @resources = @LONCAPA::map::resources;
             my @resparms = @LONCAPA::map::resparms;
             my @zombies = @LONCAPA::map::zombies;
-            my ($errors,%ids,%hidden);
+            my ($errors,%ids,%hidden,%titles,%ordered);
             $errors =
                 &recurse_supplemental($cnum,$cdom,'supplemental.sequence',
-                                      $errors,$possdel,\%ids,\%hidden);
+                                      $errors,$possdel,\%ids,\%hidden,'',\%titles,\%ordered);
             @LONCAPA::map::order = @order;
             @LONCAPA::map::resources = @resources;
             @LONCAPA::map::resparms = @resparms;
@@ -19757,6 +19757,8 @@
             $supplemental = {
                                ids => \%ids,
                                hidden => \%hidden,
+                               titles => \%titles,
+                               ordered => \%ordered,
                             };
             &Apache::lonnet::do_cache_new('supplemental',$hashid,$supplemental,600);
         }
@@ -19765,8 +19767,9 @@
 }
 
 sub recurse_supplemental {
-    my ($cnum,$cdom,$suppmap,$errors,$possdel,$suppids,$hiddensupp,$hidden) = @_;
-    if (($suppmap) && (ref($suppids) eq 'HASH') && (ref($hiddensupp) eq 'HASH')) {
+    my ($cnum,$cdom,$suppmap,$errors,$possdel,$suppids,$hiddensupp,$hidden,$supptitles,$supporder) = @_;
+    if (($suppmap) && (ref($suppids) eq 'HASH') && (ref($hiddensupp) eq 'HASH') &&
+        (ref($supptitles) eq 'HASH') && (ref($supporder) eq 'HASH')) {
         my $mapnum;
         if ($suppmap eq 'supplemental.sequence') {
             $mapnum = 0;
@@ -19784,14 +19787,16 @@
                 foreach my $idx (@order) {
                     my ($title,$src,$ext,$type,$status)=split(/\:/,$resources[$idx]);
                     if (($src ne '') && ($status eq 'res')) {
+                        push(@{$supporder->{$mapnum}},$idx);
                         my $id = $mapnum.':'.$idx;
                         push(@{$suppids->{$src}},$id);
+                        $supptitles->{$id} = $title;
                         if (($hidden) || (&get_supp_parameter($resparms[$idx],'parameter_hiddenresource') =~ /^yes/i)) {
                             $hiddensupp->{$id} = 1;
                         }
                         if ($src =~ m{^\Q/uploaded/$cdom/$cnum/\E(supplemental_\d+\.sequence)$}) {
                             $errors = &recurse_supplemental($cnum,$cdom,$1,$errors,$possdel,$suppids,
-                                                            $hiddensupp,$hiddensupp->{$id});
+                                                            $hiddensupp,$hiddensupp->{$id},$supptitles,$supporder);
                         } else {
                             my $allowed;
                             if (($env{'request.role.adv'}) || (!$hiddensupp->{$id})) {
Index: loncom/build/Makefile
diff -u loncom/build/Makefile:1.228 loncom/build/Makefile:1.229
--- loncom/build/Makefile:1.228	Sun Aug  4 00:24:25 2024
+++ loncom/build/Makefile	Wed Dec 31 23:44:29 2025
@@ -1,6 +1,6 @@
 # The LearningOnline Network with CAPA
 
-# $Id: Makefile,v 1.228 2024/08/04 00:24:25 raeburn Exp $
+# $Id: Makefile,v 1.229 2025/12/31 23:44:29 raeburn Exp $
 
 # TYPICAL USAGE of this Makefile is primarily for two targets:
 # "make build" and "make install".
@@ -151,7 +151,8 @@
 	@echo "wrap_setuid: put a C wrapper around setuid scripts."
 	@echo "bash_config_check: test if enable-bracketed-paste set to on." 
 	@echo "systemd_config_check: test if ProtectHome set to readonly."  
-        @echo "latex_fixup: regenerate ls-R database for the latex base."
+	@echo "axe_config_check: set up axe-core/puppeteer for node.js."  
+	@echo "latex_fixup: regenerate ls-R database for the latex base."
 	@echo "picins_check: check for picins.sty, retrieve and rebuild"
 	@echo "             filename databases used by LaTeX"
 	@echo "mimetex_version_check: check if mimetex.cgi version has changed,"
@@ -563,6 +564,11 @@
 	perl piml_parse.pl  $(CATEGORY) $(DIST) "$(TARGET)" $(LAUNCH) | \
 	tee -a WARNINGS
 
+axe_config_check:
+	cat $(SOURCE)/doc/loncapafiles/axe_config_check.piml | \
+	perl piml_parse.pl  $(CATEGORY) $(DIST) "$(TARGET)" $(LAUNCH) | \
+	tee -a WARNINGS
+
 postinstall:
 	make postaboutVERSION
 	make webserverconf
@@ -590,6 +596,7 @@
 	make systemd_config_check 
 	make latex_fmtutil
 	make lcmathcomplex
+	make axe_config_check  
 	sed -i "s/\x08\x08*/.../g" WARNINGS
 VERSION:
 	install -d $(TARGET)/etc
Index: loncom/loncapa_apache.conf
diff -u loncom/loncapa_apache.conf:1.287 loncom/loncapa_apache.conf:1.288
--- loncom/loncapa_apache.conf:1.287	Thu Jul 31 15:15:37 2025
+++ loncom/loncapa_apache.conf	Wed Dec 31 23:44:30 2025
@@ -2,7 +2,7 @@
 ## loncapa_apache.conf -- Apache HTTP LON-CAPA configuration file
 ##
 
-# $Id: loncapa_apache.conf,v 1.287 2025/07/31 15:15:37 raeburn Exp $
+# $Id: loncapa_apache.conf,v 1.288 2025/12/31 23:44:30 raeburn Exp $
 
 #
 # LON-CAPA Section (extensions to httpd.conf daemon configuration)
@@ -40,6 +40,7 @@
 Alias /zipspool/ /home/httpd/zipspool/
 Alias /prtspool/ /home/httpd/prtspool/
 Alias /captchaspool/ /home/httpd/captchaspool/
+Alias /axespool/ /home/httpd/axespool/
 Alias /webdav/ /home/httpd/html/priv/
 ScriptAlias /cgi-bin/ "/home/httpd/cgi-bin/"
 <IfModule mod_dav_fs.c>
@@ -399,6 +400,17 @@
 ErrorDocument     413 /adm/overloaded.txt
 ErrorDocument	  500 /adm/errorhandler
 </LocationMatch>
+
+<LocationMatch "/axespool">
+AuthType LONCAPA
+Require valid-user
+PerlAuthzHandler Apache::lonacc
+ErrorDocument     403 /adm/login
+ErrorDocument     404 /adm/notfound.html
+ErrorDocument     406 /adm/roles
+ErrorDocument     413 /adm/overloaded.txt
+ErrorDocument     500 /adm/errorhandler
+</LocationMatch>
 # ------------------------------------------------------------------------- RAT
 
 <LocationMatch "^/+priv/.*\.sequence$">
@@ -1828,6 +1840,20 @@
 </IfModule>
 </Directory>
 
+# Allow serving of files in axespool
+
+<Directory "/home/httpd/axespool/">
+Options FollowSymLinks
+AllowOverride None
+<IfModule mod_authz_core.c>
+  Require all granted
+</IfModule>
+<IfModule !mod_authz_core.c>
+  order allow,deny
+  allow from all
+</IfModule>
+</Directory>
+
 <DirectoryMatch "^/home/httpd/html/priv/.+/">
    DirectoryIndex disabled
 </DirectoryMatch>
@@ -1890,6 +1916,8 @@
 PerlSetVar       lonLTIDir    /home/httpd/lonLTItmp
 PerlSetVar       ltiIDsDir    /home/httpd/ltiIDs
 PerlSetVar       lonFontsDir     /home/httpd/html/adm/fonts
+PerlSetVar       lonNodejsDir /home/httpd/node.js
+PerlSetVar       lonAxeDir    /home/httpd/axespool
 # & separated list of % separated fields in order of
 # - internal name to call it, 
 # - regexp that it should match (done case-insensitively)
Index: doc/loncapafiles/loncapafiles.lpml
diff -u doc/loncapafiles/loncapafiles.lpml:1.1086 doc/loncapafiles/loncapafiles.lpml:1.1087
--- doc/loncapafiles/loncapafiles.lpml:1.1086	Thu Nov 13 22:48:34 2025
+++ doc/loncapafiles/loncapafiles.lpml	Wed Dec 31 23:44:31 2025
@@ -2,7 +2,7 @@
  "http://lpml.sourceforge.net/DTD/lpml.dtd">
 <!-- loncapafiles.lpml -->
 
-<!-- $Id: loncapafiles.lpml,v 1.1086 2025/11/13 22:48:34 raeburn Exp $ -->
+<!-- $Id: loncapafiles.lpml,v 1.1087 2025/12/31 23:44:31 raeburn Exp $ -->
 
 <!--
 
@@ -434,6 +434,20 @@
 </directory>
 <directory dist='default'>
   <protectionlevel>modest_delete</protectionlevel>
+  <targetdir dist='default'>home/httpd/node.js</targetdir>
+  <categoryname>server standard</categoryname>
+  <description>for Node.js scripts</description>
+</directory>
+<directory dist='default'>
+  <protectionlevel>modest_delete</protectionlevel>
+  <targetdir dist='default'>home/httpd/node.js/axe</targetdir>
+  <categoryname>server standard</categoryname>
+  <description>
+For Node.js scripts for accessibility testing with axe-core/puppeteer.
+  </description>
+</directory>
+<directory dist='default'>
+  <protectionlevel>modest_delete</protectionlevel>
   <targetdir dist='default'>home/httpd/lonLTItmp</targetdir>
   <categoryname>server standard</categoryname>
   <description>for temporary storage of LTI nonces</description>
@@ -3777,6 +3791,7 @@
 Docs_About_My_Personal_Info.tex;
 Docs_About_Simple_Page.tex;
 Docs_About_Syllabus.tex;
+Docs_Accessibility_Check.tex;
 Docs_Adding_Course_Doc.tex;
 Docs_Adding_External_Resource.tex;
 Docs_Adding_External_Tool.tex;
@@ -5139,6 +5154,46 @@
 </file>
 
 <file>
+  <source>loncom/node.js/axe/axeperl.js</source>
+  <target dist='default'>home/httpd/node.js/axe/axeperl.js</target>
+  <categoryname>interface file</categoryname>
+  <description>
+    JavaScript for Accessibility Checking using Node.js, axe-core 
+    and puppeteer
+  </description>
+</file>
+<file>
+  <source>loncom/node.js/axe/package.json</source>
+  <target dist='default'>home/httpd/node.js/axe/package.json</target>
+  <categoryname>interface file</categoryname>
+  <description>
+    JSON file used to prepare Node.js for Accessibility Checking
+    and puppeteer
+  </description>
+</file>
+<file>
+  <source>loncom/node.js/axe/AxeRunner.pm</source>
+  <target dist='default'>home/httpd/lib/perl/LONCAPA/AxeRunner.pm</target>
+  <categoryname>system file</categoryname>
+  <description>
+    Access to headless browser for accessibility checking
+  </description>
+  <status>works/verified</status>
+</file>
+<file>
+<source>loncom/node.js/axe/lonaxe.pl</source>
+<target dist='default'>home/httpd/cgi-bin/lonaxe.pl</target>
+<categoryname>script</categoryname>
+<description>
+cgi script to perform accessibility checking
+</description>
+Depends on loncapa_apache.conf entry:
+Alias /axespool/ /home/httpd/axespool/
+as well as a /home/httpd/axespool directory.
+</note>
+</file>
+
+<file>
   <source>loncom/html/adm/MathJax/mathjax-MathJax-v2.7.9.zip</source>
   <target dist='default'>home/httpd/html/adm/MathJax</target>
   <categoryname>script</categoryname>
@@ -8574,6 +8629,7 @@
 <categoryname>graphic file</categoryname>
 <description>graphical icons used in submenus</description>
 <filenames>
+accessibility.png;
 archive.png;
 aboutme.png;
 accesstimes.png;

Index: loncom/node.js/axe/axeperl.js
+++ loncom/node.js/axe/axeperl.js
const { AxePuppeteer } = require('@axe-core/puppeteer');
const fs = require('fs');
const readline = require('readline');

const buffer = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false
});

const {getBrowser, closeBrowser} = require('/home/httpd/node.js/axe/browser.js');

let browser = Start();
let wcag = null;
let level = null;

buffer.on('line', async (line) => {
  try {
    if (line.substr(0, 7).trim() === 'COOKIE') await setcookie(JSON.parse(line.substr(7)));
    else if (line.substr(0, 5).trim() === 'WCAG') await setwcag(line.substr(5));
    else if (line.substr(0, 4).trim() === 'URL') await getViolations(line.substr(4));
    else if (line === 'END') Exit();
    else Error('Unknown command ' + line.split(' ', 1));
  } catch (err) {
    Error(err.message.replace(/\n/g, '\\n'));
  }
});

async function setcookie(cookie) {
  if (cookie.cookieid === 'lonSID') {
    await browser.setCookie(
    {
      name: cookie.cookieid,
      value: cookie.handle,
      path: '/',
      secure: true,
      domain: cookie.hostname,
    },
    {
      name: 'lonLinkID',
      value: cookie.lonLinkID,
      path: '/',
      domain: cookie.hostname,
    });
    console.log('SETCOOKIE');
  } else if (cookie.cookieid === 'lonID') {
    await browser.setCookie(
    {
      name: cookie.cookieid,
      value: cookie.handle,
      path: '/',
      domain: cookie.hostname,
    });
    console.log('SETCOOKIE');
  }
}

async function setwcag(compliance) {
   const values = compliance.split(":");
   wcag = values[0];
   level = values[1];
   console.log('SETWCAG');
}

async function getViolations(url) {
  let tags = [];
  if (wcag === '2') {
    tags = [wcag+level];
  } else if (wcag === '21') {
    tags = ['wcag2'+level, wcag+level];
  } else if (wcag === '22') {
    tags = ['wcag2'+level, 'wcag21'+level, wcag+level];
  }
  const page = await browser.newPage();
  await page.goto(url);
  const results = await new AxePuppeteer(page).withTags(tags).analyze();
  const violations = results.violations;
  const count = violations.length;
  if (count > 0) {
    console.log(count+' '+JSON.stringify(violations));
  } else {
    console.log(count);
  }
  await page.close();
}

async function Exit() {
  await closeBrowser();
  console.log('END');
  process.exit();
}

function Error(message) {
  console.log('ERROR ' + message);
}

async function Start() {
  browser = await getBrowser();
  console.log('START');
}

Index: loncom/node.js/axe/package.json
+++ loncom/node.js/axe/package.json
{
  "name": "axeperl",
  "version": "1.0.0",
  "description": "Use axe-core for accessibility testing",
  "main": "axeperl.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "accessibility"
  ],
  "author": "Stuart Raeburn",
  "license": "GPL-2.0-or-later",
  "dependencies": {
    "@axe-core/puppeteer": "^4.10.2",
    "puppeteer": "^24.24.1"
  }
}

Index: loncom/node.js/axe/AxeRunner.pm
+++ loncom/node.js/axe/AxeRunner.pm
# The LearningOnline Network with CAPA
# Accessibility Testing
#
# $Id: AxeRunner.pm,v 1.1 2025/12/31 23:44:28 raeburn Exp $
#
# The LearningOnline Network with CAPA
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# LON-CAPA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LON-CAPA; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# /home/httpd/html/adm/gpl.txt
#
# http://www.lon-capa.org/
#

package LONCAPA::AxeRunner;

use strict;
use IPC::Open2;
use JSON::DWIW;
use Data::Dumper;
use lib '/home/httpd/lib/perl';
use LONCAPA::Configuration();

our ($chld_out, $chld_in, $pid);

BEGIN {
    my $perlvarref=&LONCAPA::Configuration::read_conf();
    my $nodejsdir;
    if (ref($perlvarref) eq 'HASH') {
        if (defined($perlvarref->{'lonNodejsDir'})) {
            $nodejsdir = $perlvarref->{'lonNodejsDir'};
        }
    }
    if (($nodejsdir) && (-e "$nodejsdir/axe/axeperl.js") &&
        (-e '/usr/bin/chromium-browser')) {
        $pid = IPC::Open2::open2($chld_out, $chld_in, "/usr/bin/node $nodejsdir/axe/axeperl.js");
        if ($pid ne '') {
            my $line = <$chld_out>;
            chomp($line);
            unless ($line eq 'START') {
                &cleanup();
            }
        }
    }
}

sub launch {
    my ($cookieid,$handle,$linkid,$hostname,$compliance) = @_;
    return if ($pid eq '');
    if (($handle ne '') && ($hostname ne '')) {
        my $cookieinfo = {
            handle => $handle,
            lonLinkID => $linkid,
            hostname => $hostname,
            cookieid => $cookieid,
        };
        print $chld_in 'COOKIE ' . JSON::DWIW::to_json($cookieinfo) . "\n";
        my $line = <$chld_out>;
        chomp($line);
        if ($line eq 'SETCOOKIE') {
            if ($compliance =~ /^(\d{1,2})(a{1,3})$/) {
                print $chld_in 'WCAG ' .$1 .':'. $2 ."\n";
                my $line = <$chld_out>;
                chomp($line);
                if ($line eq 'SETWCAG') {
                    return 'ok';
                }
            }
        }
    }
    return;
}

sub checkcompliance {
    my ($url) = @_;
    $url =~ s{^\s+|\s+$}{}g;
    if ($url =~ m{^https?://[\w.]+/}) {
        if ($pid) {
            print $chld_in 'URL ' . $url . "\n";
            my $line = <$chld_out>;
            chomp($line);
            if ($line eq '0') {
                return ($line);
            } elsif ($line =~ /^(\d+)\s+/) {
                my $count = $1;
                $line =~s /^(\d+)\s+//;
                return ($count,Data::Dumper->Dump(JSON::DWIW::deserialize($line)));
            }
        } else {
             return;
        }
    } else {
        return (-1);
    }
}

sub cleanup {
    if ($pid ne '') {
        print $chld_in "END\n";
        close($chld_in);
        close($chld_out);
        waitpid( $pid, 0 );
        my $child_exit_status = $? >> 8;
        if ($child_exit_status == 0) {
            undef($pid);
            return 'ok';
        }
    }
}

END {
   &cleanup();
}

1;
__END__

=pod

=head1 NAME

B<LONCAPA::AxeRunner> - Access headless browser for accessibility checking.

=head1 SYNOPSIS

 use lib '/home/httpd/lib/perl/';
 use LONCAPA::AxeRunner;

 if (&LONCAPA::AxeRunner::launch ( $cookieid,$handle,$linkid,$hostname ) eq 'ok') {
     my ($violations,$details) =
         &LONCAPA::AxeRunner::checkcompliance( $url );
     &LONCAPA::AxeRunner::end();
 }

=head1 DESCRIPTION

Requires nodejs, puppeteer, and headless chromium browser.
Uses IPC::Open2::open2() to send data to nodejs script
and to receive data back.

The following methods are available:

=over 4

=item LONCAPA::AxeRunner::launch ( $handle,$linkid,$hostname );

=back

=over 4

=item LONCAPA::AxeRunner::checkcompliance( $url );

=back

=over 4

=item LONCAPA::AxeRunner::cleanup ();

=back

=head1 AUTHORS

This library is free software; you can redistribute it and/or
modify it under the same terms as LON-CAPA itself.

=cut

Index: loncom/node.js/axe/lonaxe.pl
+++ loncom/node.js/axe/lonaxe.pl
#!/usr/bin/perl
$|=1;
# $Id: lonaxe.pl,v 1.1 2025/12/31 23:44:28 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# LON-CAPA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LON-CAPA; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# /home/httpd/html/adm/gpl.txt
#
# http://www.lon-capa.org/
#

use strict;

use lib '/home/httpd/lib/perl/';
use Apache::lonlocal;
use Apache::loncommon;
use Apache::lonnet;
use LONCAPA::Configuration;
use LONCAPA::loncgi;
use LONCAPA::AxeRunner;

if (! &LONCAPA::loncgi::check_cookie_and_load_env()) {
    print <<END;
Content-type: text/html

<html>
<head><title>Bad Cookie</title></head>
<body>
Your cookie information is incorrect.
</body>
</html>
END
    exit;
}

my $identifier = $ENV{'QUERY_STRING'};
my $filename = $env{'cgi.'.$identifier.'.file'};
my $nummain = $env{'cgi.'.$identifier.'.main'};
my $numsupp = $env{ 'cgi.'.$identifier.'.supp'};
my $crstype = $env{ 'cgi.'.$identifier.'.crstype'};
my %from_cgi_env = (
                     uname => $env{'cgi.'.$identifier.'.user'},
                     udom  => $env{'cgi.'.$identifier.'.domain'},
                     cnum  => $env{'cgi.'.$identifier.'.cnum'},
                     cdom  => $env{'cgi.'.$identifier.'.cdom'},
                   );
my %perlvar=%{&LONCAPA::Configuration::read_conf('loncapa.conf')};
&Apache::lonlocal::get_language_handle();
&Apache::loncommon::content_type(undef,'text/html');
$env{'request.noversionuri'} = '/cgi-bin/lonaxe.pl';

# Breadcrumbs
my $brcrum = [{'href' => '/adm/coursedocs?tools=1',
               'text' => "$crstype Editor"},
              {'href' => '',
               'text' => "Choose Resources"},
              {'href' => '',
               'text' => 'Accessibility Results'}];

print &Apache::loncommon::start_page('Accessibility Checking',
                                     undef,
                                     {'bread_crumbs' => $brcrum,});

my $linked_id = $env{'user.linkedenv'};
my $sessionfile = $env{'user.environment'};
my $lonidsdir = $perlvar{'lonIDsDir'};
my $lonhost = $perlvar{'lonHostID'};
my $hostname = &Apache::lonnet::hostname($lonhost);
my $wcag = '22aa';
my $handle;

if ($sessionfile =~ m{^\Q$lonidsdir\E/([^/]+)\.id$}) {
    $handle = $1;
    my ($cookieid,$protocol);
    if ($linked_id) {
        $cookieid = 'lonSID';
        $protocol = 'https'; 
    } else {
        $cookieid = 'lonID';
        $protocol = 'http';
    }
    if (&LONCAPA::AxeRunner::launch($cookieid,$handle,$linked_id,$hostname,$wcag) eq 'ok') {
        my $number_of_files = $nummain + $numsupp;
        if ($number_of_files) {
            my %prog_state = &Apache::lonhtmlcommon::Create_PrgWin('',$number_of_files);
            print "<br />";
            if (open(my $fh,'<',$filename)) {
                my @items;
                while (my $line = <$fh>) {
                    chomp($line);
                    push(@items,$line);
                }
                close($fh);
                foreach my $item (@items) {
                    my ($type,$dest,$title) = split(/\0/,$item);
                    my $querystr = '?inhibitmenu=yes';
                    my $url = $dest;
                    if ($type eq 'M') {
                        $querystr .= '&symb='.$dest;
                        $url = &Apache::lonnet::clutter((&Apache::lonnet::decode_symb($dest))[2]);
                    }
                    if ($url ne '') { 
print STDERR 'Check for ||'.$protocol.'://'.$hostname.$url.$querystr."||\n";
                        my ($violations,$details) =
                            &LONCAPA::AxeRunner::checkcompliance($protocol.'://'.$hostname.$url.$querystr);
                        &Apache::lonhtmlcommon::Increment_PrgWin('',\%prog_state,'last resource');
                        if ($violations eq -1) {
                            print "Invalid compliance level or invalid URL\n";
                        } elsif ($violations eq '') {
                            print "No listener available to receive URL\n";
                        } elsif ($violations == 0) {
                            print "No violations for $url\n";
                        } else {
                            print "$violations violations for $url\n".'<br />'.$details."\n";
                        }
                    }
                }
            }
            &Apache::lonhtmlcommon::Close_PrgWin('',\%prog_state);
            print "<br />";
        }
    }
    unless (&LONCAPA::AxeRunner::cleanup() eq 'ok') {
        print "Did not exit normally.\n";
    }
} else {
    print "Launch failed.\n";
    unless (&LONCAPA::AxeRunner::cleanup() eq 'ok') {
        print "Did not exit normally.\n";
    }
}
print &Apache::loncommon::end_page();
exit;


Index: loncom/html/adm/help/tex/Docs_Accessibility_Check.tex
+++ loncom/html/adm/help/tex/Docs_Accessibility_Check.tex
\label{Docs_Accessibility_Check}

Course Editor $>$ Content Utilities $>$ Check for Accessibility can be used to perform accessibility checking for selected course content.

Use the checkboxes to indicate which resource(s) are to be checked from the ``Main Content'' area, and/or from the ``Supplemental Content'' area (if any exist in the course).

Index: doc/loncapafiles/axe_config_check.piml
+++ doc/loncapafiles/axe_config_check.piml
<!DOCTYPE piml PUBLIC "-//TUX/DTD piml 1.0 Final//EN" 
	"http://lpml.sourceforge.net/DTD/piml.dtd">
<!-- axe_config_check.piml -->

<!-- $Id: axe_config_check.piml,v 1.1 2025/12/31 23:44:31 raeburn Exp $ -->

<!--

This file is part of the LearningOnline Network with CAPA (LON-CAPA).

LON-CAPA is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

LON-CAPA is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with LON-CAPA; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

/home/httpd/html/adm/gpl.txt

http://www.lon-capa.org/

-->

<piml>
<targetroot>/</targetroot>
<files>
<file>
<target dist="default">/home/httpd/nodejs/axe</target>
<perlscript mode="fg">
use strict;
use Cwd;

my $addexport = 1;

if (-e '/home/www/.bashrc') {
    if (open(my $fh,'<','/home/www/.bashrc')) {
        while my $line (<fh>) {
            chomp($line);
            if ($line =~ m{^\Qexport PUPPETEER_EXECUTABLE_PATH="/usr/bin/chromium-browser"\E$}) {
                $addexport = 0;
                last;
            }
        }
        close($fh);
    }
    if ($addexport) {
        if (open(my $fh,'>>','/home/www/.bashrc')) {
            print $fh 'export PUPPETEER_EXECUTABLE_PATH="/usr/bin/chromium-browser"'."\n";
            close($fh);
        }
    }
} else {
    if (open(my $fh,'>','/home/www/.bashrc')) {
        print $fh 'export PUPPETEER_EXECUTABLE_PATH="/usr/bin/chromium-browser"'."\n";
        close($fh);
        chown()
    }
}

my $original_dir = cwd();
my $original_uid = $gt;; 
my $wwwuid = getpwnam('www');
my $wwwgid = getgrnam('www');

$( = $wwwgid;
$> = $wwwuid;
if (chdir '/home/httpd/nodejs/axe') {
    if (open(PIPE,"npm install|")) {
        my @lines = (<PIPE>);
        close(PIPE);
        chomp(@lines);
        foreach my $line (@lines) {
            print "$line\n";
        }
    }
}
$> = $<;
chdir $original_dir;

</perlscript>
</file>
</files>
</piml>


More information about the LON-CAPA-cvs mailing list