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   45230 use 5.006_001;
  2         5  
  2         61  
5              
6 2     2   7 use strict;
  2         2  
  2         47  
7 2     2   6 use warnings;
  2         3  
  2         68  
8              
9             our $VERSION = '0.21';
10              
11 2     2   7 use Carp;
  2         2  
  2         131  
12 2     2   13 use File::Basename;
  2         2  
  2         178  
13 2     2   553 use Encode qw/decode/;
  2         7990  
  2         2232  
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 239     239   252 my ( $filename ) = @_;
93              
94 239         1654 my @pieces = split(m/[\\\/]/, $filename);
95 239         400 my $filename_without_path = pop(@pieces);
96 239         6604 my ($name, $path, $suffix) = fileparse($filename_without_path, qr/\.[^.]*/);
97 239         767 return ($name, $suffix);
98             }
99              
100             sub get_shortcut_name_from_filename {
101 108     108 0 1537 my ( $filename ) = @_;
102 108         176 my ($name, $suffix) = _fileparse_any_extension($filename);
103 108         212 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 23 my ( $filename ) = @_;
116            
117 10         19 my ($name, $suffix) = _fileparse_any_extension($filename);
118            
119 10         51 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 117     117 1 44621 my ( $filename ) = @_;
150              
151 117         191 my ($name, $suffix) = _fileparse_any_extension($filename);
152            
153 117 100       422 if (!exists($_shortcut_file_readers{lc ($suffix)})) {
154 1         140 croak ( "Shortcut file does not have a recognized extension!" );
155             }
156 116         191 my $reader_sub = $_shortcut_file_readers{lc ($suffix)};
157 116         217 &$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 464 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 1441     1441   1476 my ( $line, $group_name ) = @_;
181              
182 1441         16627 return $line =~ m/^\s*\[${group_name}\]\s*$/;
183             }
184              
185             sub _get_key_value_pair {
186 436     436   415 my ( $line ) = @_;
187 436 100       2225 if($line =~ m/^\s*([A-Za-z0-9-]*)(\[([^\[\]]*)\])?\s*=\s*([^\n\r]*?)\s*$/) {
188 434         1330 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 155     155   165 my ( $line ) = @_;
196              
197 155         554 return $line =~ m/^\s*(#.*)?$/;
198             }
199              
200             sub _url_is_blank_or_comment_line {
201 300     300   322 my ( $line ) = @_;
202              
203 300         254 my $elimination_line = $line;
204 300         288 $elimination_line =~ s/(;.*)//;
205 300         845 $elimination_line =~ s/\s*//;
206              
207             # Account for other separators as well.
208 300         541 return $elimination_line eq "";
209             }
210              
211              
212              
213              
214             ### The readers
215              
216             sub _ensure_file_exists {
217 86     86   68 my ( $filename ) = @_;
218            
219 86 100       1560 unless(-e $filename) {
220 2         445 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 32     32 1 39 my ( $filename ) = @_;
244            
245 32         52 _ensure_file_exists($filename);
246 1 50   1   10 open (my $file, "<:encoding(UTF-8)", $filename) or croak ( "Error opening file ${filename}: $!" );
  1         1  
  1         9  
  31         916  
247            
248 31         3324 my $url = read_desktop_shortcut_handle($file);
249            
250 27         264 close ($file);
251            
252 27         54 my $name = get_shortcut_name_from_filename($filename);
253              
254             return {
255 27         176 "name", $name,
256             "url", $url};
257             }
258              
259              
260              
261             sub read_url_shortcut_file {
262 54     54 1 60 my ( $filename ) = @_;
263            
264 54         76 _ensure_file_exists($filename);
265 53 50       1295 open (my $file, "<", $filename) or croak ( "Error opening file ${filename}: $!" );
266            
267 53         107 my $url = read_url_shortcut_handle($file);
268              
269 52         319 close ($file);
270            
271 52         80 my $name = get_shortcut_name_from_filename($filename);
272              
273             return {
274 52         296 "name", $name,
275             "url", $url};
276             }
277              
278              
279             sub read_website_shortcut_file {
280 20     20 1 34 read_url_shortcut_file(@_);
281             }
282              
283              
284              
285             sub read_webloc_shortcut_file
286             {
287 30     30 1 44 my ( $filename ) = @_;
288            
289 30 100       1467 open (my $file, "<", $filename) or croak ( "Error opening file ${filename}: $!" );
290 29         79 binmode($file);
291            
292 29         75 my $url = read_webloc_shortcut_handle( $file );
293            
294 27         230 close ($file);
295            
296 27         56 my $name = get_shortcut_name_from_filename($filename);
297              
298             return {
299 27         161 "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 31     31 1 35 my ( $file ) = @_;
324            
325             # Make sure that we are using line feed as the separator
326 31         117 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 31         46 my $desktop_entry_found = 0;
331 31         27 while(1) {
332 37         495 my $next_line = <$file>;
333 37 100       408 if(not $next_line) {
    100          
    100          
334             # End of file
335 1         3 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 29         33 $desktop_entry_found = 1;
339 29         40 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 31 100       88 if (not $desktop_entry_found) {
349 2         44 die "Desktop Entry group not found in desktop file.";
350             }
351            
352 29         32 my $type = undef;
353 29         30 my $url = undef;
354 29         104 while(1) {
355 177         459 my $next_line = <$file>;
356 177 100       389 if(not $next_line) {
    100          
    100          
357 28         43 last;
358             } elsif(_is_group_header($next_line, ".*")) {
359 1         3 last;
360             } elsif(_desktop_entry_is_blank_or_comment_line($next_line)) {
361             # Ignore this line
362             } else {
363 139         167 my ($key, $locale, $value) = _get_key_value_pair($next_line);
364 139 100       219 if(defined($key)) {
365 138 100       537 if($key eq "Type") {
    100          
366 28         50 $type = $value;
367             } elsif($key eq "URL") {
368 27         53 $url = $value;
369             }
370             } else {
371 1         46 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 29 100       86 if(!defined($type)) {
    100          
378 1         33 warn "Warning: Type not found in desktop file";
379             } elsif($type ne "Link") {
380 1         38 warn "Warning: Invalid type ${type} in desktop file";
381             }
382            
383 29 100       59 if(not defined($url)) {
384 2         99 die "URL not found in file";
385             }
386              
387 27         91 return $url;
388             }
389              
390              
391             use constant {
392 2         1155 NO_SECTION => 0,
393             INTERNET_SHORTCUT_SECTION => 1,
394             INTERNET_SHORTCUT_W_SECTION => 2,
395             OTHER_SECTION => 3,
396 2     2   13 };
  2         2  
397              
398             sub read_url_shortcut_handle {
399 53     53 1 57 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 53         171 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 53         71 my $curr_section = NO_SECTION;
408 53         44 my $parsed_url = undef;
409 53         46 my $parsed_urlw = undef;
410 53         37 while(1) {
411 509         8608 my $next_line = <$file>;
412 509 100       806 if(not $next_line) {
    100          
    100          
    100          
    100          
413 53         59 last;
414             # use a constant instead of indicvidual bools.
415             } elsif(_is_group_header($next_line, "InternetShortcut")) {
416 53         90 $curr_section = INTERNET_SHORTCUT_SECTION;
417             } elsif(_is_group_header($next_line, "InternetShortcut.W")) {
418 6         47 $curr_section = INTERNET_SHORTCUT_W_SECTION
419             } elsif(_is_group_header($next_line, ".*")) {
420 97         235 $curr_section = OTHER_SECTION;
421             } elsif(_url_is_blank_or_comment_line($next_line)) {
422             # Ignore this line
423             } else {
424 297         353 my ($key, $locale, $value) = _get_key_value_pair($next_line);
425 297 100       417 if(defined($key)) {
426 296 100       771 if($key eq "URL") {
427 64 100       104 if($curr_section == INTERNET_SHORTCUT_SECTION) {
    100          
428 52         122 $parsed_url = $value;
429             } elsif($curr_section == INTERNET_SHORTCUT_W_SECTION) {
430 6         22 $parsed_urlw = decode("UTF-7", $value);
431             }
432             }
433             } else {
434 1         62 warn "Warning: Found a line in the file with no valid key/value pair: ${next_line}";
435             }
436             }
437             }
438              
439 53         29 my $url;
440 53 100       97 if(defined($parsed_urlw)) {
    100          
441 6         8 $url = $parsed_urlw;
442             } elsif(defined($parsed_url)) {
443 46         48 $url = $parsed_url;
444             } else {
445 1         14 die "URL not found in file";
446             }
447              
448 52         130 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   40 my ( $module, $list ) = @_;
460              
461 2 50   2   500 eval ( "use ${module} ${list}; 1" ) or
  2     2   24733  
  2     2   323  
  2     2   13  
  2     1   3  
  2     1   232  
  2     1   10  
  2     1   4  
  2     1   175  
  2     1   11  
  2     1   2  
  2     1   180  
  1     1   7  
  1     1   1  
  1     1   87  
  1     1   8  
  1     1   2  
  1     1   84  
  1     1   5  
  1     1   1  
  1     1   76  
  1     1   7  
  1     1   2  
  1     1   101  
  1     1   8  
  1         2  
  1         91  
  1         7  
  1         2  
  1         89  
  1         9  
  1         2  
  1         86  
  1         8  
  1         3  
  1         88  
  1         7  
  1         2  
  1         94  
  1         7  
  1         2  
  1         84  
  1         7  
  1         2  
  1         84  
  1         6  
  1         1  
  1         86  
  1         9  
  1         1  
  1         88  
  1         9  
  1         2  
  1         90  
  1         7  
  1         2  
  1         84  
  1         5  
  1         1  
  1         81  
  1         8  
  1         2  
  1         88  
  1         7  
  1         2  
  1         85  
  1         8  
  1         2  
  1         97  
  1         9  
  1         2  
  1         89  
  1         6  
  1         2  
  1         86  
  29         2474  
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 46 my ( $file ) = @_;
470              
471 29         64 _try_load_module_for_webloc ( "Mac::PropertyList", "qw(:all)" );
472              
473 29         81 my $data = parse_plist_fh( $file );
474            
475 29 100       115383 if (ref($data) ne "Mac::PropertyList::dict") {
    100          
476 1         28 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         37 my $url_object = $data->{ 'URL' };
482 27         59 my $url = $url_object->value;
483              
484 27         383 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 10 my ( $filename ) = @_;
499            
500 4         10 my ($name, $suffix) = _fileparse_any_extension($filename);
501            
502 4 50       19 if (!exists($_shortcut_handle_readers{lc ($suffix)})) {
503 0         0 croak ( "Shortcut file does not have a recognized extension!" );
504             }
505 4         8 my $reader_sub = $_shortcut_handle_readers{lc ($suffix)};
506 4         51 return $reader_sub;
507             }
508              
509              
510              
511             1;
512             __END__