File Coverage

blib/lib/WebShortcutUtil/Read.pm
Criterion Covered Total %
statement 227 229 99.1
branch 64 68 94.1
condition n/a
subroutine 52 53 98.1
pod 12 13 92.3
total 355 363 97.8


line stmt bran cond sub pod time code
1             package WebShortcutUtil::Read;
2              
3              
4 2     2   484155 use 5.006_001;
  2         9  
  2         91  
5              
6 2     2   13 use strict;
  2         3  
  2         86  
7 2     2   12 use warnings;
  2         5  
  2         112  
8              
9             our $VERSION = '0.20';
10              
11 2     2   14 use Carp;
  2         4  
  2         161  
12 2     2   18 use File::Basename;
  2         5  
  2         214  
13 2     2   790 use Encode qw/decode/;
  2         241958  
  2         3394  
14              
15             require Exporter;
16              
17             our @ISA = qw(Exporter);
18              
19             our %EXPORT_TAGS = ( 'all' => [ qw(
20             shortcut_has_valid_extension
21             get_shortcut_name_from_filename
22             read_shortcut_file
23             read_shortcut_file_url
24             read_desktop_shortcut_file
25             read_url_shortcut_file
26             read_webloc_shortcut_file
27             read_website_shortcut_file
28             get_handle_reader_for_file
29             read_desktop_shortcut_handle
30             read_url_shortcut_handle
31             read_webloc_shortcut_handle
32             read_website_shortcut_handle
33             ) ] );
34              
35             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
36              
37             our @EXPORT = qw(
38             );
39              
40             =head1 NAME
41              
42             WebShortcutUtil::Read - Utilities for reading web shortcut files
43              
44             =head1 SYNOPSIS
45              
46             use WebShortcutUtil::Read qw(
47             shortcut_has_valid_extension
48             read_shortcut_file
49             read_shortcut_file_url);
50              
51             # If the file does not have a valid shortcut extension, the reads will fail.
52             if(!shortcut_has_valid_extension($filename)) {
53             die "File is not a shortcut!";
54             }
55              
56             # Read name and URL
57             my $results = read_shortcut_file($filename);
58             my $name = $results->{"name"};
59             my $url = $results->{"url"};
60              
61             # Just get URL
62             my $url = read_shortcut_file_url($filename);
63              
64             =head1 DESCRIPTION
65              
66             The following subroutines are provided:
67              
68             =over 4
69              
70             =cut
71              
72             my %_shortcut_file_readers = (
73             ".desktop", \&read_desktop_shortcut_file,
74             ".url", \&read_url_shortcut_file,
75             ".webloc", \&read_webloc_shortcut_file,
76             ".website", \&read_website_shortcut_file,
77             );
78              
79             my %_shortcut_handle_readers = (
80             ".desktop", \&read_desktop_shortcut_handle,
81             ".url", \&read_url_shortcut_handle,
82             ".webloc", \&read_webloc_shortcut_handle,
83             ".website", \&read_website_shortcut_handle,
84             );
85              
86              
87              
88             ### Routines that deal with file names
89              
90             sub _fileparse_any_extension {
91 239     239   246 my ( $filename ) = @_;
92              
93 239         2752 my @pieces = split(m/[\\\/]/, $filename);
94 239         411 my $filename_without_path = pop(@pieces);
95 239         8614 my ($name, $path, $suffix) = fileparse($filename_without_path, qr/\.[^.]*/);
96 239         835 return ($name, $suffix);
97             }
98              
99              
100             =item get_shortcut_name_from_filename( FILENAME )
101              
102             Gets the shortcut name from the file name. This essentially
103             gets the base name of the file name. This is the same name
104             that the shortcut readers will return.
105              
106             =cut
107              
108             sub get_shortcut_name_from_filename {
109 108     108 1 1964 my ( $filename ) = @_;
110 108         212 my ($name, $suffix) = _fileparse_any_extension($filename);
111 108         198 return $name;
112             }
113              
114              
115             =item shortcut_file_has_valid_extension( FILENAME )
116              
117             Checks the specified file name and returns true if
118             its extension matches one of the supported types.
119              
120             =cut
121              
122             sub shortcut_has_valid_extension {
123 10     10 0 22 my ( $filename ) = @_;
124              
125 10         24 my ($name, $suffix) = _fileparse_any_extension($filename);
126              
127 10         61 return exists $_shortcut_file_readers{lc ($suffix)};
128             }
129              
130              
131             =item read_shortcut_file( FILENAME )
132              
133             Reads the specified file and extracts the contents. The type of
134             shortcut file is determined by the file extension. A hash will be returned
135             containing two keys: "name" and "url". The name is the name/title of
136             the shortcut. A hash will always be returned - if there is an error
137             reading the file, the routine will die with an appropriate error message.
138              
139             For ".desktop" and ".url" files, the reader can handle unicode characters
140             in the name and URL. ".webloc" files may contain unicode characters as well,
141             although this functionality still requires more testing.
142              
143             The name returned by the readers is a guess. Many shortcut files
144             do not contain a name/title embedded in the file. ".desktop" shortcuts
145             may contain several embedded names with different encodings. Unfortunately,
146             these names are not necessarily updated when the shortcut is renamed.
147             It is difficult, if not impossible, to determine which is the correct name.
148             As of right now, the reader will always return the name of the file as the
149             name of the shortcut, although this may change in the future.
150              
151             Note: The Mac::PropertyList module (http://search.cpan.org/~bdfoy/Mac-PropertyList/)
152             must be installed in order to read ".webloc" files.
153              
154             =cut
155              
156             sub read_shortcut_file {
157 117     117 1 60754 my ( $filename ) = @_;
158              
159 117         228 my ($name, $suffix) = _fileparse_any_extension($filename);
160              
161 117 100       429 if (!exists($_shortcut_file_readers{lc ($suffix)})) {
162 1         185 croak ( "Shortcut file does not have a recognized extension!" );
163             }
164 116         186 my $reader_sub = $_shortcut_file_readers{lc ($suffix)};
165 116         250 &$reader_sub($filename);
166             }
167              
168              
169             =item read_shortcut_file_url( FILENAME )
170              
171             The same as read_shortcut_file, but only returns a string containing the URL.
172              
173             =cut
174              
175             sub read_shortcut_file_url {
176 1     1 1 586 return read_shortcut_file(@_)->{"url"};
177             }
178              
179              
180              
181             ### Routines used by the readers to parse lines
182              
183             # Note that the dollar sign at the end of the regular
184             # expression matches the new line at the end of the
185             # string.
186              
187             sub _is_group_header {
188 1431     1431   1679 my ( $line, $group_name ) = @_;
189              
190 1431         20663 return $line =~ m/^\s*\[${group_name}\]\s*$/;
191             }
192              
193             sub _get_key_value_pair {
194 434     434   460 my ( $line ) = @_;
195 434 100       2780 if($line =~ m/^\s*([A-Za-z0-9-]*)(\[([^\[\]]*)\])?\s*=\s*([^\n\r]*?)\s*$/) {
196 432         1546 return ($1, $3, $4);
197             } else {
198 2         8 return (undef, undef, undef);
199             }
200             }
201              
202             sub _desktop_entry_is_blank_or_comment_line {
203 155     155   174 my ( $line ) = @_;
204              
205 155         634 return $line =~ m/^\s*(#.*)?$/;
206             }
207              
208             sub _url_is_blank_or_comment_line {
209 298     298   328 my ( $line ) = @_;
210              
211 298         347 my $elimination_line = $line;
212 298         337 $elimination_line =~ s/(;.*)//;
213 298         897 $elimination_line =~ s/\s*//;
214              
215             # Account for other separators as well.
216 298         624 return $elimination_line eq "";
217             }
218              
219              
220              
221              
222             ### The readers
223              
224             sub _ensure_file_exists {
225 86     86   146 my ( $filename ) = @_;
226              
227 86 100       1848 unless(-e $filename) {
228 2         478 croak "File ${filename} does not exist!";
229             }
230             }
231              
232             =item read_desktop_shortcut_file( FILENAME )
233              
234             =item read_url_shortcut_file( FILENAME )
235              
236             =item read_website_shortcut_file( FILENAME )
237              
238             =item read_webloc_shortcut_file( FILENAME )
239              
240             These routines operate essentially the same way as read_shortcut_file.
241             However, they force the file to be parsed as a particular type,
242             regardless of the file extension. These should be used sparingly.
243             You should use read_shortcut_file unless you have a good
244             reason not to.
245              
246             =cut
247              
248             # SEE REFERENCES IN WebShortcutUtil.pm
249              
250             sub read_desktop_shortcut_file {
251 32     32 1 42 my ( $filename ) = @_;
252              
253 32         59 _ensure_file_exists($filename);
254 1 50   1   9 open (my $file, "<:encoding(UTF-8)", $filename) or croak ( "Error opening file ${filename}: $!" );
  1         1  
  1         9  
  31         1164  
255              
256 31         3476 my $url = read_desktop_shortcut_handle($file);
257              
258 27         304 close ($file);
259              
260 27         66 my $name = get_shortcut_name_from_filename($filename);
261              
262             return {
263 27         200 "name", $name,
264             "url", $url};
265             }
266              
267              
268              
269             sub read_url_shortcut_file {
270 54     54 1 68 my ( $filename ) = @_;
271              
272 54         97 _ensure_file_exists($filename);
273 53 50       1709 open (my $file, "<", $filename) or croak ( "Error opening file ${filename}: $!" );
274              
275 53         121 my $url = read_url_shortcut_handle($file);
276              
277 52         388 close ($file);
278              
279 52         108 my $name = get_shortcut_name_from_filename($filename);
280              
281             return {
282 52         360 "name", $name,
283             "url", $url};
284             }
285              
286              
287             sub read_website_shortcut_file {
288 20     20 1 39 read_url_shortcut_file(@_);
289             }
290              
291              
292              
293             sub read_webloc_shortcut_file
294             {
295 30     30 1 47 my ( $filename ) = @_;
296              
297 30 100       1374 open (my $file, "<", $filename) or croak ( "Error opening file ${filename}: $!" );
298 29         61 binmode($file);
299              
300 29         69 my $url = read_webloc_shortcut_handle( $file );
301              
302 27         261 close ($file);
303              
304 27         66 my $name = get_shortcut_name_from_filename($filename);
305              
306             return {
307 27         190 "name", $name,
308             "url", $url};
309             }
310              
311              
312              
313              
314              
315             =item read_desktop_shortcut_handle( HANDLE )
316              
317             =item read_url_shortcut_handle( HANDLE )
318              
319             =item read_website_shortcut_handle( HANDLE )
320              
321             =item read_webloc_shortcut_handle( HANDLE )
322              
323             Similar to the corresponding file readers, but read from an
324             IO::Handle object instead.
325             =cut
326              
327             sub read_desktop_shortcut_handle {
328 31     31 1 46 my ( $handle ) = @_;
329              
330             # Make sure that we are using line feed as the separator
331 31         134 local $/ = "\n";
332              
333             # Read to the "Desktop Entry"" group - this should be the first entry, but comments and blank lines are allowed before it.
334             # Should handle desktop entries at different positions not just in first spot....
335 31         38 my $desktop_entry_found = 0;
336 31         26 while(1) {
337 37         563 my $next_line = <$handle>;
338 37 100       464 if(not $next_line) {
    100          
    100          
339             # End of file
340 1         3 last;
341             # Per the Desktop Entry specifications, [KDE Desktop Entry] was used at one time...
342             } elsif(_is_group_header($next_line, "(KDE )?Desktop Entry")) {
343 29         34 $desktop_entry_found = 1;
344 29         51 last;
345             } elsif(_desktop_entry_is_blank_or_comment_line($next_line)) {
346             # Ignore this line
347             } else {
348             # When we find a line that does not match the above criteria, stop looping. This should never happen.
349 1         2 last;
350             }
351             }
352              
353 31 100       87 if (not $desktop_entry_found) {
354 2         54 die "Desktop Entry group not found in desktop file.";
355             }
356              
357 29         35 my $type = undef;
358 29         35 my $url = undef;
359 29         27 while(1) {
360 177         460 my $next_line = <$handle>;
361 177 100       325 if(not $next_line) {
    100          
    100          
362 28         33 last;
363             } elsif(_is_group_header($next_line, ".*")) {
364 1         2 last;
365             } elsif(_desktop_entry_is_blank_or_comment_line($next_line)) {
366             # Ignore this line
367             } else {
368 139         213 my ($key, $locale, $value) = _get_key_value_pair($next_line);
369 139 100       246 if(defined($key)) {
370 138 100       398 if($key eq "Type") {
    100          
371 28         52 $type = $value;
372             } elsif($key eq "URL") {
373 27         48 $url = $value;
374             }
375             } else {
376 1         104 warn "Warning: Found a line in the file with no valid key/value pair: ${next_line}";
377             }
378             }
379             }
380              
381             # Show a warning if the Type key is not right, but still continue.
382 29 100       95 if(!defined($type)) {
    100          
383 1         58 warn "Warning: Type not found in desktop file";
384             } elsif($type ne "Link") {
385 1         62 warn "Warning: Invalid type ${type} in desktop file";
386             }
387              
388 29 100       60 if(not defined($url)) {
389 2         40 die "URL not found in file";
390             }
391              
392 27         103 return $url;
393             }
394              
395              
396             use constant {
397 2         1420 NO_SECTION => 0,
398             INTERNET_SHORTCUT_SECTION => 1,
399             INTERNET_SHORTCUT_W_SECTION => 2,
400             OTHER_SECTION => 3,
401 2     2   19 };
  2         3  
402              
403             sub read_url_shortcut_handle {
404 53     53 1 91 my ( $handle ) = @_;
405              
406             # Make sure that we are using line feed as the separator.
407             # Windows uses \r\n as the terminator, but should be safest always to use \n since it
408             # handles both end-of-line cases.
409 53         206 local $/ = "\n";
410              
411             # Read to the desktop file entry group - this should be the first entry, but comments and blank lines are allowed before it.
412 53         61 my $curr_section = NO_SECTION;
413 53         56 my $parsed_url = undef;
414 53         101 my $parsed_urlw = undef;
415 53         56 while(1) {
416 505         11273 my $next_line = <$handle>;
417 505 100       941 if(not $next_line) {
    100          
    100          
    100          
    100          
418 53         78 last;
419             # use a constant instead of indicvidual bools.
420             } elsif(_is_group_header($next_line, "InternetShortcut")) {
421 52         95 $curr_section = INTERNET_SHORTCUT_SECTION;
422             } elsif(_is_group_header($next_line, "InternetShortcut.W")) {
423 6         16 $curr_section = INTERNET_SHORTCUT_W_SECTION
424             } elsif(_is_group_header($next_line, ".*")) {
425 96         248 $curr_section = OTHER_SECTION;
426             } elsif(_url_is_blank_or_comment_line($next_line)) {
427             # Ignore this line
428             } else {
429 295         391 my ($key, $locale, $value) = _get_key_value_pair($next_line);
430 295 100       465 if(defined($key)) {
431 294 100       947 if($key eq "URL") {
432 62 100       124 if($curr_section == INTERNET_SHORTCUT_SECTION) {
    100          
433 51         133 $parsed_url = $value;
434             } elsif($curr_section == INTERNET_SHORTCUT_W_SECTION) {
435 6         32 $parsed_urlw = decode("UTF-7", $value);
436             }
437             }
438             } else {
439 1         59 warn "Warning: Found a line in the file with no valid key/value pair: ${next_line}";
440             }
441             }
442             }
443              
444 53         45 my $url;
445 53 100       134 if(defined($parsed_urlw)) {
    100          
446 6         15 $url = $parsed_urlw;
447             } elsif(defined($parsed_url)) {
448 46         57 $url = $parsed_url;
449             } else {
450 1         18 die "URL not found in file";
451             }
452              
453 52         174 return $url;
454             }
455              
456              
457             sub read_website_shortcut_handle {
458 0     0 1 0 read_url_shortcut_handle(@_);
459             }
460              
461             # TODO: Fix this eval to not use an expression. This causes it to fail perlcritic.
462             sub _try_load_module_for_webloc {
463 29     29   165 my ( $module, $list ) = @_;
464              
465 2 50   2   559 eval ( "use ${module} ${list}; 1" ) or
  2     2   28764  
  2     2   343  
  2     2   15  
  2     1   4  
  2     1   282  
  2     1   10  
  2     1   3  
  2     1   181  
  2     1   16  
  2     1   4  
  2     1   299  
  1     1   5  
  1     1   2  
  1     1   94  
  1     1   5  
  1     1   2  
  1     1   95  
  1     1   5  
  1     1   2  
  1     1   83  
  1     1   4  
  1     1   2  
  1     1   82  
  1     1   5  
  1         1  
  1         82  
  1         5  
  1         1  
  1         83  
  1         5  
  1         2  
  1         82  
  1         6  
  1         1  
  1         82  
  1         5  
  1         2  
  1         85  
  1         7  
  1         1  
  1         113  
  1         8  
  1         1  
  1         101  
  1         5  
  1         1  
  1         83  
  1         5  
  1         2  
  1         88  
  1         5  
  1         2  
  1         82  
  1         6  
  1         2  
  1         88  
  1         6  
  1         1  
  1         83  
  1         5  
  1         1  
  1         84  
  1         6  
  1         3  
  1         105  
  1         5  
  1         2  
  1         90  
  1         6  
  1         2  
  1         93  
  1         5  
  1         2  
  1         84  
  29         2224  
466             die "Could not load ${module} module. This module is required in order to read/write webloc files. Error: $@";
467             }
468              
469              
470              
471             sub read_webloc_shortcut_handle
472             {
473 29     29 1 40 my ( $handle ) = @_;
474              
475 29         68 _try_load_module_for_webloc ( "Mac::PropertyList", "qw(:all)" );
476              
477 29         88 my $data = parse_plist_fh( $handle );
478              
479 29 100       162188 if (ref($data) ne "Mac::PropertyList::dict") {
    100          
480 1         34 die "Webloc plist file does not contain a dictionary!";
481             } elsif(!exists($data->{ 'URL' })) {
482 1         16 die "Webloc plist file does not contain a URL!";
483             }
484              
485 27         53 my $url_object = $data->{ 'URL' };
486 27         71 my $url = $url_object->value;
487              
488 27         369 return $url;
489             }
490              
491             =item get_handle_reader_for_file( FILENAME )
492              
493             Gets the handle reader (either read_desktop_shortcut_handle, read_url_shortcut_handle,
494             read_website_shortcut_handle, or read_webloc_shortcut_handle) to read the
495             specified file. This routine is useful if you need to read from an IO::Handle
496             object, and have the original file name to determine the shortcut type.
497              
498             =cut
499              
500             sub get_handle_reader_for_file
501             {
502 4     4 1 11 my ( $filename ) = @_;
503              
504 4         10 my ($name, $suffix) = _fileparse_any_extension($filename);
505              
506 4 50       17 if (!exists($_shortcut_handle_readers{lc ($suffix)})) {
507 0         0 croak ( "Shortcut file does not have a recognized extension!" );
508             }
509 4         9 my $reader_sub = $_shortcut_handle_readers{lc ($suffix)};
510 4         57 return $reader_sub;
511             }
512              
513              
514              
515             1;
516             __END__