[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