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 11 13 84.6
total 354 363 97.5


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