File Coverage

blib/lib/Xcruciate/Utils.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package Xcruciate::Utils;
2              
3 2     2   12 use Exporter;
  2         5  
  2         149  
4             @ISA = ('Exporter');
5             @EXPORT = qw();
6             our $VERSION = 0.21;
7              
8 2     2   16 use strict;
  2         4  
  2         53  
9 2     2   17 use warnings;
  2         3  
  2         52  
10 2     2   1757 use Time::gmtime;
  2         12465  
  2         124  
11 2     2   18 use Carp;
  2         4  
  2         105  
12 2     2   1248 use XML::LibXML;
  0            
  0            
13             use XML::LibXSLT;
14              
15             =head1 NAME
16              
17             Xcruciate::Utils - Utilities for Xcruciate
18              
19             =head1 SYNOPSIS
20              
21             check_path('A very nice path',$path,'rw');
22              
23             =head1 DESCRIPTION
24              
25             Provides utility functions Xcruciate ( F).
26              
27             =head1 AUTHOR
28              
29             Mark Howe, Emelonman@cpan.orgE
30              
31             =head2 EXPORT
32              
33             None
34              
35             =head1 FUNCTIONS
36              
37             =head2 check_path(option,path,permissions[,non_fatal])
38              
39             Checks that the path exists, and that it has the appropriate
40             permissions, where permissions contains some combination of r, w and x. If not, and if non_fatal is perlishly false,
41             it dies, using the value of option to produce a semi-intelligable error message. If non_fatal is perlishly true it returns the error or an empty string.
42              
43             =cut
44              
45             sub check_path {
46             my $option = shift;
47             my $path = shift;
48             my $permissions = shift;
49             my $non_fatal = 0;
50             $non_fatal = 1 if $_[0];
51             my $error = "";
52             if ( not( -e $path ) ) {
53             $error = "No file corresponding to path for '$option'";
54             }
55             elsif ( $permissions =~ /r/ and ( not -r $path ) ) {
56             $error = "File '$path' for '$option' option is not readable";
57             }
58             elsif ( $permissions =~ /w/ and ( not -w $path ) ) {
59             $error = "File '$path' for '$option' option is not writable";
60             }
61             elsif ( $permissions =~ /x/ and ( not -x $path ) ) {
62             $error = "File '$path' for '$option' option is not executable";
63             }
64             if ($non_fatal) {
65             return $error;
66             }
67             else {
68             croak $error;
69             }
70             }
71              
72             =head2 check_absolute_path(option,path,permissions[,non_fatal])
73              
74             A lot like &check_path (which it calls), but also checks that the path is
75             absolute (ie is starts with a /).
76              
77             =cut
78              
79             sub check_absolute_path {
80             my $option = shift;
81             my $path = shift;
82             my $permissions = shift;
83             my $non_fatal = 0;
84             $non_fatal = 1 if defined $_[0];
85             if ( $path !~ m!^/! and $non_fatal ) {
86             return "Path for '$option' must be absolute";
87             }
88             elsif ( $path !~ m!^/! ) {
89             croak "Path for '$option' must be absolute";
90             }
91             else {
92             check_path( $option, $path, $permissions, $non_fatal );
93             }
94             }
95              
96             =head2 type_check(path,name,value,record)
97              
98             Returns errors on typechecking value against record. Name is provided for error messages. Path is from config file.
99              
100             =cut
101              
102             sub type_check {
103             my $path = shift;
104             my $name = shift;
105             my $value = shift;
106             my $record = shift;
107             $value =~ s/^\s*(.*?)\s*$/$1/s;
108             my @errors = ();
109             my $list_name = '';
110             $list_name = "Item $_[0] of" if defined $_[0];
111             my $datatype = $record->[2];
112              
113             if ( $datatype eq 'integer' ) {
114             push @errors,
115             sprintf( "$list_name Entry called %s should be an integer", $name )
116             unless $value =~ /^\d+$/;
117             push @errors,
118             sprintf(
119             "$list_name Entry called %s is less than minimum permitted value of $record->[3]",
120             $name )
121             if ( $value =~ /^\d+$/
122             and ( defined $record->[3] )
123             and ( $record->[3] > $value ) );
124             push @errors,
125             sprintf(
126             "$list_name Entry called %s exceeds permitted value of $record->[4]",
127             $name )
128             if ( $value =~ /^\d+$/
129             and ( defined $record->[4] )
130             and ( $record->[4] < $value ) );
131             }
132             elsif ( $datatype eq 'float' ) {
133             push @errors,
134             sprintf( "$list_name Entry called %s should be a number", $name )
135             unless $value =~ /^-?\d+(\.\d+)?$/;
136             push @errors,
137             sprintf(
138             "$list_name Entry called %s is less than minimum permitted value of $record->[3]",
139             $name )
140             if ( $value =~ /^-?\d+(\.\d+)$/
141             and ( defined $record->[3] )
142             and ( $record->[3] > $value ) );
143             push @errors,
144             sprintf(
145             "$list_name Entry called %s exceeds permitted value of $record->[4]",
146             $name )
147             if ( $value =~ /^-?\d+(\.\d+)$/
148             and ( defined $record->[4] )
149             and ( $record->[4] < $value ) );
150             }
151             elsif ( $datatype eq 'ip' ) {
152             push @errors,
153             sprintf( "$list_name Entry called %s should be an ip address", $name )
154             unless $value =~ /^\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?$/;
155             }
156             elsif ( $datatype eq 'cidr' ) {
157             push @errors,
158             sprintf( "$list_name Entry called %s should be a CIDR ip range",
159             $name )
160             unless $value =~ m!^\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?/\d\d?$!;
161             }
162             elsif ( $datatype eq 'yes_no' ) {
163             push @errors,
164             sprintf( "$list_name Entry called %s should be 'yes' or 'no'", $name )
165             unless $value =~ /^(yes)|(no)$/;
166             }
167             elsif ( $datatype eq 'duration' ) {
168             push @errors,
169             sprintf(
170             "$list_name Entry called %s should be a duration (eg PT2H30M, P15DT12H)",
171             $name )
172             unless $value =~ /^-?P(\d+D)?(T(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/;
173             }
174             elsif ( $datatype eq 'word' ) {
175             push @errors,
176             sprintf(
177             "$list_name Entry called %s should be a word (ie no whitespace)",
178             $name )
179             unless $value =~ /^\S+$/;
180             }
181             elsif ( $datatype eq 'hexbyte' ) {
182             push @errors,
183             sprintf(
184             "$list_name Entry called %s should be a hexidecimal byte (00 - FF)",
185             $name )
186             unless $value =~ /^[0-9A-F][0-9A-F]$/;
187             }
188             elsif ( $datatype eq 'captchastyle' ) {
189             push @errors,
190             sprintf( "$list_name Entry called %s should be a captcha style",
191             $name )
192             unless $value =~ /^rect|default|circle|ellipse|ec|box|blank$/;
193             }
194             elsif ( $datatype eq 'language' ) {
195             push @errors,
196             sprintf( "$list_name Entry called %s should be a language code",
197             $name )
198             unless $value =~ /^[a-z][a-z]$/;
199             }
200             elsif ( $datatype eq 'function_name' ) {
201             push @errors,
202             sprintf(
203             "$list_name Entry called %s should be an xpath function name",
204             $name )
205             unless $value =~ /^[^\s:]+(:\S+)?$/;
206             }
207             elsif ( $datatype eq 'path' ) {
208             push @errors,
209             sprintf( "$list_name Entry called %s should be a path", $name )
210             unless $value =~ /^\S+$/;
211             }
212             elsif ( $datatype eq 'url' ) {
213             push @errors,
214             sprintf( "$list_name Entry called %s should be a url", $name )
215             unless $value =~ /^(\/)|(http)/;
216             }
217             elsif ( $datatype eq 'imagesize' ) {
218             push @errors,
219             sprintf(
220             "$list_name Entry called %s should be an image size (123x456)",
221             $name )
222             unless $value =~ /^\d+x\d+$/;
223             }
224             elsif ( $datatype eq 'dateformat' ) {
225             push @errors,
226             sprintf( "$list_name Entry called %s should be a time format", $name )
227             unless $value =~ /\S/;
228             }
229             elsif ( $datatype eq 'timeoffset' ) {
230             push @errors,
231             sprintf( "$list_name Entry called %s should be a time zone offset",
232             $name )
233             unless $value =~ /^(-1[01])|(1[012])|(-?[1-9])|0$/;
234             }
235             elsif ( $datatype eq 'email' ) {
236             push @errors,
237             sprintf( "$list_name Entry called %s should be an email address",
238             $name )
239             unless $value =~ /^[^\s@]+\@[^\s@]+$/;
240             }
241             elsif ( ( $datatype eq 'abs_file' ) or ( $datatype eq 'abs_dir' ) ) {
242             $value = "$path/$value" if ( $path and $value !~ /^\// );
243             push @errors,
244             sprintf(
245             "$list_name Entry called %s should be absolute (ie it should start with /)",
246             $name )
247             unless $value =~ /^\//;
248             push @errors,
249             sprintf(
250             "No file or directory corresponds to $list_name entry called %s ('%s')",
251             $name, $value )
252             unless -e $value;
253             if ( -e $value ) {
254             push @errors,
255             sprintf(
256             "$list_name Entry called %s should be a file, not a directory",
257             $name )
258             if ( ( -d $value ) and ( $datatype eq 'abs_file' ) );
259             push @errors,
260             sprintf(
261             "$list_name Entry called %s should be a directory, not a file",
262             $name )
263             if ( ( -f $value ) and ( $datatype eq 'abs_dir' ) );
264             push @errors,
265             sprintf( "$list_name Entry called %s must be readable", $name )
266             if ( $record->[3] =~ /r/ and not -r $value );
267             push @errors,
268             sprintf( "$list_name Entry called %s must be writable", $name )
269             if ( $record->[3] =~ /w/ and not -w $value );
270             push @errors,
271             sprintf( "$list_name Entry called %s must be executable", $name )
272             if ( $record->[3] =~ /x/ and not -x $value );
273             push @errors, check_file_content( $name, $value, $record->[4] )
274             if ( ( -f $value ) and $record->[4] );
275             }
276             }
277             elsif ( $datatype eq 'abs_create' ) {
278             $value = "$path/$value" if ( $path and $value !~ /^\// );
279             $value =~ m!^(.*/)?([^/]+$)!;
280             my $dir = $1;
281             push @errors,
282             sprintf(
283             "$list_name Entry called %s should be absolute (ie it should start with /)",
284             $name )
285             unless $value =~ /^\//;
286             push @errors,
287             sprintf(
288             "$list_name No file or directory corresponds to entry called %s, and insufficient rights to create one",
289             $name )
290             if (
291             ( not -e $value )
292             and ( ( not $dir )
293             or ( -d $dir )
294             and ( ( not -r $dir ) or ( not -w $dir ) or ( not -x $dir ) ) )
295             );
296             push @errors,
297             sprintf( "$list_name Entry called %s must be readable", $name )
298             if ( $record->[3] =~ /r/ and -e $value and not -r $value );
299             push @errors,
300             sprintf( "$list_name Entry called %s must be writable", $name )
301             if ( $record->[3] =~ /w/ and -e $value and not -w $value );
302             push @errors,
303             sprintf( "$list_name Entry called %s must be executable", $name )
304             if ( $record->[3] =~ /x/ and -e $value and not -x $value );
305             }
306             elsif ( $datatype eq 'debug_list' ) {
307             if ( $value !~ /,/ ) {
308             push @errors,
309             sprintf( "$list_name Entry called %s cannot include '%s'",
310             $name, $value )
311             unless $value =~
312             /^((none)|(all)|(timer-io)|(non-timer-io)|(io)|(show-wrappers)|(connections)|(doc-cache)|(doc-write)|(channels)|(stack)|(update)|(verbose)|(result)|(backup))$/;
313             }
314             else {
315             foreach my $v ( split /\s*,\s*/, $value ) {
316             push @errors,
317             sprintf(
318             "$list_name Entry called %s cannot include 'all' or 'none' in a comma-separated list",
319             $name )
320             if $v =~ /^((none)|(all))$/;
321             push @errors,
322             sprintf( "$list_name Entry called %s cannot include '%s'",
323             $name, $v )
324             unless $v =~
325             /^((none)|(all)|(timer-io)|(non-timer-io)|(io)|(show-wrappers)|(connections)|(doc-cache)|(channels)|(stack)|(update)|(verbose)|(result)|(backup))$/;
326             }
327             }
328             }
329             else {
330             croak sprintf( "ERROR: Unknown unit config datatype %s", $datatype );
331             }
332             return @errors;
333             }
334              
335             =head2 check_file_content
336              
337             Check an XML or XSLT file
338              
339             =cut
340              
341             sub check_file_content {
342             my $name = shift;
343             my $filename = shift;
344             my $type = shift;
345             my @ret = ();
346             if ( $type !~ /^((xsl)|(xml))$/ ) {
347             push @ret, "Unknown file content type '$type'";
348             }
349             else {
350             my $parser = XML::LibXML->new();
351             eval { my $xml_parser = $parser->parse_file($filename) };
352             push @ret,
353             "Could not parse file for entry '$name' ('$filename') as XML: $@"
354             if $@;
355             }
356             return @ret;
357             }
358              
359             =head2 parse_xslt(file_path)
360              
361             Attempts to parse a file as XSLT 1.0 and returns an error in case of failure (ie false means 'no error').
362              
363             =cut
364              
365             sub parse_xslt {
366             my $filename = shift;
367             my $ret = '';
368             my $parser = XML::LibXML->new();
369             my $xml_parser;
370             eval { $xml_parser = $parser->parse_file($filename) };
371             if ($@) {
372             my $errormsg = $@;
373             $errormsg =~ s/ at .*?$//gs;
374             $ret = "Could not parse '$filename' as XML: $errormsg";
375             }
376             else {
377             my $xslt_parser = XML::LibXSLT->new();
378             eval { my $stylesheet = $xslt_parser->parse_stylesheet($xml_parser) };
379             if ($@) {
380             my $errormsg = $@;
381             $errormsg =~ s/ at .*?$//gs;
382             $ret = "Could not parse '$filename' as XSLT: $errormsg";
383             }
384             }
385             return $ret;
386             }
387              
388             =head2 apache_time(epoch_time)
389              
390             Produces an apache-style timestamp from an epoch time.
391              
392             =cut
393              
394             sub apache_time {
395             my $epoch_time = shift;
396             my $time = gmtime($epoch_time);
397             my @days = qw(Sun Mon Tue Wed Thu Fri Sat);
398             my @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
399             return sprintf(
400             "%s, %02d %s %04d %02d:%02d:%02d GMT",
401             $days[ $time->wday ],
402             $time->mday,
403             $months[ $time->mon ],
404             $time->year + 1900,
405             $time->hour, $time->min, $time->sec
406             );
407             }
408              
409             =head2 datetime(epoch_time)
410              
411             Converts GMT epoch time to the format expected by XSLT date functions.
412              
413             =cut
414              
415             sub datetime
416             { #Converts GMT epoch time to the format expected by XSLT date functions
417             my $epoch_time = shift;
418             my $time = gmtime($epoch_time);
419             return sprintf(
420             "%04d-%02d-%02dT%02d:%02d:%02d+00:00",
421             $time->year + 1900,
422             $time->mon + 1,
423             $time->mday, $time->hour, $time->min, $time->sec
424             );
425             }
426              
427             =head2 duration_in_seconds(schemaduration)
428              
429             Converts an XML Schema duration into seconds (Month and Year must be zero or absent for compatibility with EXSLT's date:seconds().
430              
431             =cut
432              
433             sub duration_in_seconds {
434             my $schema_duration = shift;
435             my $epoch_duration = 0;
436             my ( $minus, $date, $time_plus_t, $time ) =
437             $schema_duration =~ /^(-)?P([0-9D]+)?(T([0-9.HMS]+))?$/;
438             if ( ( not defined $date ) and ( not defined $time ) ) {
439             return undef;
440             }
441             else {
442             if ( defined $date ) {
443             $date =~ /^((\d+)D)$/;
444             $epoch_duration += $2 * 86400 if $2;
445             }
446             if ( defined $time ) {
447             $time =~ /^(((\d+)H)?)(((\d+)M)?)(((\d+(\.\d+)?)S))?$/;
448              
449             #print "Values $3:$6:$9";
450             $epoch_duration += $3 * 3600 if $3;
451             $epoch_duration += $6 * 60 if $6;
452             $epoch_duration += $9 if $9;
453             }
454             $epoch_duration = 0 - $epoch_duration if $minus;
455             return $epoch_duration;
456             }
457             }
458              
459             =head2 index_docroot($docroot_path,$mimetypes_hash)
460              
461             Returns XML describing the contents of $docroot_path.
462              
463             =cut
464              
465             sub index_docroot {
466             my $docroot = shift;
467             my $mimetypes = shift;
468             my $ndirs = 0;
469             my $nfiles = 0;
470              
471             my $dir_xml;
472             my $dir_writer = XML::Writer->new( OUTPUT => \$dir_xml );
473             $dir_writer->startTag("directories");
474              
475             opendir( DIR, $docroot ) or croak "Cannot opendir '$docroot': $!";
476             while ( defined( my $file = readdir(DIR) ) ) {
477             next unless $file =~ /^[^.\s]+$/;
478             next unless -d "$docroot/$file";
479             $ndirs++;
480             $dir_writer->startTag(
481             "directory",
482             "url_path" => $file,
483             "local_path" => $file
484             );
485             opendir( DIR2, "$docroot/$file" )
486             or croak "Cannot opendir '$docroot/$file': $!";
487             while ( defined( my $file2 = readdir(DIR2) ) ) {
488             next unless $file2 =~ /^[^.\s]+\.([^.\s~%]+)$/;
489             my $suffix = $1;
490             next unless -f "$docroot/$file/$file2";
491             $nfiles++;
492             $dir_writer->emptyTag(
493             "file",
494             "url_name" => $file2,
495             "local_name" => $file2,
496             "size" => ( -s "$docroot/$file/$file2" ),
497             "utime" => Xcruciate::Utils::datetime(
498             ( stat("$docroot/$file/$file2") )[9]
499             ),
500             "document_type" => ( $mimetypes->{$suffix} || 'text/plain' )
501             );
502             }
503             closedir(DIR2);
504             $dir_writer->endTag;
505             }
506             closedir(DIR);
507              
508             $dir_writer->endTag;
509             $dir_writer->end;
510              
511             return $dir_xml;
512             }
513              
514             =head1 BUGS
515              
516             The best way to report bugs is via the Xcruciate bugzilla site (F).
517              
518             =head1 PREVIOUS VERSIONS
519              
520             B<0.01>: First upload
521              
522             B<0.03>: First upload containing module
523              
524             B<0.04>: Changed minimum perl version to 5.8.8
525              
526             B<0.05>: Added debug_list data type, fixed uninitialised variable error when numbers aren't.
527              
528             B<0.07>: Attempt to put all Xcruciate modules in one PAUSE tarball.
529              
530             B<0.08>: Added index_docroot (previously inline code in xcruciate script)
531              
532             B<0.09>: Fixed typo in error message. Use Carp for errors. Non-fatal option for check_path()
533              
534             B<0.10>: Prepend path entry to relative paths
535              
536             B<0.12>: Resolve modifiable file paths, attempt to parse XML and XSLT files
537              
538             B<0.13>: Do not attempt to parse XSLT as part of config file validation (because modifiable XSLT files
539             will not be in place for a clean install). Add explicit function to test XSLT later.
540              
541             B<0.14>: Add doc-write to permissible debug options.
542              
543             B<0.15>: Dot optional in number data type. Remove last line of XSLT parse errors.
544              
545             B<0.16>: Integers acceptable where float requested. Added duration data type.
546              
547             B<0.17>: use warnings.
548              
549             B<0.18>: dateformat, url and timeoffset data types.
550              
551             B<0.19>: duration_in_seconds(). Better duration type checking.
552              
553             B<0.20>: Example durations in error message now legal durations. Added hexbyte, captchastyle and imagesize types.
554              
555             =head1 COPYRIGHT AND LICENSE
556              
557             Copyright (C) 2007 - 2009 by SARL Cyberporte/Menteith Consulting
558              
559             This library is distributed under BSD licence (F).
560              
561             =cut
562              
563             1;