#!/usr/bin/env perl
use strict;
use warnings;
use Carp;
use Pod::Usage qw( pod2usage );
use Getopt::Long qw( :config gnu_getopt );
use version; my $VERSION = qv('0.3.0');
use English qw( -no_match_vars );
use WWW::Mechanize;
use IO::Prompt qw( prompt );
use File::Slurp qw( slurp );
use Config::Tiny;
use Path::Class qw( file );
use List::MoreUtils qw( uniq );
# Integrated logging facility
use Log::Log4perl qw( :easy :no_extra_logdie_message );
Log::Log4perl->easy_init($INFO);
my %defaults = (
config => file($ENV{HOME}, '.zooomr')->stringify(),
login_page => 'http://www.zooomr.com/login/',
upload_page => 'http://www.zooomr.com/photos/upload/?noflash=okiwil
+l',
logout_page => 'http://www.zooomr.com/logout/',
search_page => 'http://www.zooomr.com/search/photos/',
login => 1, # by default, try to login
pause => 5,
backoff => 5,
max_retry => 4,
debug => $INFO,
);
# Script implementation here
my %config = get_configuration(%defaults);
if ($config{check}) {
print {*STDERR} "configuration OK\n";
exit 0;
}
get_logger()->level(
{
TRACE => $TRACE,
DEBUG => $DEBUG,
INFO => $INFO,
WARN => $WARN,
ERROR => $ERROR,
FATAL => $FATAL,
}->{uc $config{debug}}
|| $INFO
);
INFO 'configuration OK';
# On with the show
my $ua = WWW::Mechanize->new(autocheck => 1);
eval {
my $action = __PACKAGE__->can($config{action})
or die
"You've found a bug! Someway, $config{action} is not supported\n"
+;
$ua->env_proxy();
$ua->proxy('http', $config{proxy}) if exists $config{proxy};
if ($config{login}) {
if ($config{cookie}) {
INFO "getting login info from cookie file '$config{cookie}'";
require HTTP::Cookies::Netscape;
my $jar = HTTP::Cookies::Netscape->new(autosave => 0);
$jar->load($config{cookie})
or die "could not load cookie file $config{cookie}\n";
$ua->cookie_jar($jar);
}
else {
INFO "logging into account $config{username}";
login();
}
}
$action->();
if ($config{login} && !$config{cookie}) {
INFO "logging out";
logout();
}
};
ERROR $EVAL_ERROR if $EVAL_ERROR;
INFO 'all operations completed';
sub get_configuration {
my %config = @_;
my $action = shift @ARGV;
$action = '--usage' unless defined $action;
# First of all, try to honor meta-options
pod2usage(message => "$0 $VERSION", -verbose => 99, -sections => ''
+)
if $action eq '--version';
pod2usage(-verbose => 99, -sections => 'USAGE') if $action eq '--us
+age';
pod2usage(-verbose => 99, -sections => 'USAGE|EXAMPLES|OPTIONS')
if $action eq '--help';
pod2usage(-verbose => 2) if $action eq '--man';
# getting here means that it's a real call for something
my @common_options = qw(
username|user|u=s password|pass|p=s proxy|P=s cookie|cookie-file|
+k=s
config|C=s check|dry-run|c! dump|d! summary|summary-file|s=s
tag|t=s@ debug|D=s
);
my %options_for = (
add => [
qw( files|files-from|f=s@ resize=i
public! private! family! friends!
backoff=i pause=i max_retry|max-retry|m=i
)
],
search => [qw( login! maxpage|max-page|m=i who|w=s )],
);
pod2usage(
message => "error: operation '$action' not supported\n",
-verbose => 99,
-sections => 'USAGE'
)
unless exists $options_for{$action};
# This will record the original parameters from different sources
my %sources = (_script => {%config});
my %cmdline;
GetOptions(\%cmdline, @common_options, @{$options_for{$action}});
%config = (%config, %cmdline); # First merge, cmdline overrides
$sources{_cmdline} = \%cmdline;
# Read defaults from configuration file, if exists
if (-e $config{config}) {
my $cfile = Config::Tiny->read($config{config});
%config = (%config, %{$cfile->{_}}, %cmdline);
$sources{_cfile} = $cfile;
} ## end if (-e $config{config})
# Record original options for validators to make changes and action
%config = (%config, %sources, action => $action, argv => [@ARGV]);
@ARGV = (); # empty, already recorded in config
# Call relevant method for parameter checking, if exists
if (my $sub = __PACKAGE__->can('validate_' . $action)) {
$sub->(\%config);
}
if ($config{dump}) {
require Data::Dumper;
local $Data::Dumper::Indent;
local $Data::Dumper::Indent = 1;
print {*STDOUT} Data::Dumper->Dump([\%config], ['configuration']
+);
} ## end if ($config{dump})
# Now check/adjust common parameters
push @{$config{tag}}, $config{tags} if defined $config{tags};
$config{tags} = join ' ', @{$config{tag} || []};
# Check username existence and prompt for password if necessary
# If cookie file has been set, don't ask for a username, will check
# later if it's all ok
if ($config{login}) {
if ($config{cookie}) {
pod2usage(
message => "error: please provide an existent cookie file\
+n",
-verbose => 99,
-sections => 'USAGE',
) unless -r $config{cookie} && -f $config{cookie};
}
else {
pod2usage(
message => "error: please provide a username or a cookie fil
+e\n",
-verbose => 99,
-sections => 'USAGE'
)
unless exists $config{username};
$config{password} = prompt 'password: ', -echo => '*'
unless exists $config{password};
}
} ## end if ($config{login} && ...
return %config;
} ## end sub get_configuration
sub login {
$ua->get($config{login_page});
$ua->form_with_fields(qw( username password ));
$ua->set_fields(
username => $config{username},
password => $config{password},
);
$ua->submit();
return;
} ## end sub login
sub logout {
$ua->get($config{logout_page});
return;
}
sub validate_add {
my $config = shift;
my %config = %$config;
# Establish real privacy, command line overrides configuration
@config{qw( private family friends )} = ()
if $config{_cmdline}{public};
$config{private} ||= $config{family} || $config{friends};
$config{public} = $config{private} ? 0 : 1;
delete $config{private};
# Ensure there's some files to work on
my @cmdline_files = @{$config{argv}};
my @filed_files = map {
chomp(my @lines = slurp($_));
@lines;
} @{$config{files} || []};
my @filenames = uniq grep {
if (-e $_) { 1 }
else {
print {*STDERR} "file '$_' does not exist, ignoring\n";
0;
}
} @cmdline_files, @filed_files;
pod2usage(
message => "error: no file to upload\n",
-verbose => 99,
-sections => 'USAGE'
)
unless @filenames;
$config{filenames} = \@filenames;
%$config = %config;
return;
} ## end sub validate_add
sub add {
INFO "starting file upload";
my $retry = 0;
my $backoff = $config{backoff};
my @results;
my @failed;
FILE:
for my $filename (@{$config{filenames}}) {
(my $barename = file($filename)->basename()) =~ s{\.\w+\z}{}mxs;
eval {
$ua->get($config{upload_page})
or die "couldn't get '$config{upload_page}'\n";
$ua->form_with_fields(qw( Filedata labels ));
$ua->set_fields(
labels => $config{tags},
is_public => $config{public},
Filedata => $filename,
);
$ua->tick('is_friend', 1, $config{friends});
$ua->tick('is_family', 1, $config{family});
if ($config{resize} && (my $resized = add_resize($filename)))
+ {
my $input = $ua->current_form()->find_input('Filedata');
$input->filename($filename); # just to be on the safe s
+ide
$input->content($resized);
}
INFO "uploading '$filename'";
$ua->submit();
};
my $error = 0; # assume no error just to
+begin
if ($EVAL_ERROR) {
ERROR $EVAL_ERROR;
$error = 1;
}
else { # Now check that the photo is actually there...
if (my $link = $ua->find_link(text => $barename)) {
INFO 'upload successful';
push @results, [ $filename, $link->url_abs() ];
}
else {
ERROR 'no error received, but the photo is not there';
$error = 1;
}
} ## end else [ if ($EVAL_ERROR)
if ($error) { # Retry scheme
if (++$retry <= $config{max_retry}) {
if ($backoff) {
INFO "sleeping $backoff second"
. ($backoff == 1 ? '' : 's');
sleep $backoff if $backoff;
$backoff *= 2; # exponential backoff
} ## end if ($backoff)
redo FILE;
} ## end if (++$retry <= $config...
ERROR "giving up on '$filename'";
push @results, [ $filename, '**FAILED**' ];
push @failed, $filename;
} ## end if ($error)
# Reset these values for next photo upload
$retry = 0;
$backoff = $config{backoff};
# Sleep a bit if configured. Avoid sleeping after last photo
sleep $config{pause}
if $config{pause}
and $filename ne $config{filenames}[-1];
} ## end for my $filename (@{$config...
# Recap on failed files
ERROR 'failed files: ', join ' | ', @failed if @failed;
# Summary, if requested
if (defined $config{summary}) {
if (open my $fh, '>', $config{summary}) {
print {$fh} "File\tURI\n";
print {$fh} join("\t", @$_), "\n" for @results;
close $fh;
}
else {
ERROR "could not open $config{summary}: $OS_ERROR";
}
}
return;
} ## end sub add
sub add_resize {
my ($filename) = @_;
require Image::Magick;
require File::Temp;
my $magick = Image::Magick->new();
my ($width, $height, $size, $format) = $magick->Ping($filename);
return if $width <= $config{resize} && $height <= $config{resize};
my ($neww, $newh);
if ($width > $height) {
$neww = $config{resize};
$newh = int($height * $neww / $width);
}
else {
$newh = $config{resize};
$neww = int($width * $newh / $height);
}
my $ecode;
$ecode = $magick->Read($filename) and die $ecode;
$ecode = $magick->Resize(width => $neww, height => $newh)
and die $ecode;
my $scaled;
my $fh = File::Temp::tempfile();
binmode $fh;
$magick->Write(file => $fh, filename => $filename);
seek $fh, 0, 0; # rewind
$scaled = slurp $fh;
close $fh;
return $scaled;
} ## end sub add_resize
sub validate_search {
my $conf = shift;
my %entry_for = (
all => 1,
social => 3,
contacts => 3,
everyone => 1,
);
$conf->{who} = $entry_for{$conf->{who} || ''};
$conf->{who} ||= 2; # default to 'me'
$conf->{who} = 1 unless $conf->{login};
return;
} ## end sub validate_search
sub search {
INFO "starting search";
$ua->get($config{search_page});
$ua->form_with_fields(qw( w q ));
$ua->select('w', {n => $config{who}});
$ua->field('q', $config{tags});
$ua->click();
my ($matches, $word) =
$ua->content() =~ /we found ([\d,]+) (photos?)/msi;
if (!defined $matches) {
INFO "no photo matching the criteria";
return;
}
my $npages = 1;
if (my @pages = $ua->find_all_links(url_regex => qr/\&page=(\d+)\z/
+mxs))
{
($npages) = $pages[-2]->url() =~ /(\d+)\z/msx;
}
my $npages_w = $npages == 1 ? 'page' : 'pages';
INFO "found $matches $word in $npages $npages_w";
my $base_url = $ua->uri();
$npages = $config{maxpage}
if defined $config{maxpage} && $npages > $config{maxpage};
for my $page_id (1 .. $npages) {
$ua->get($base_url . "&page=$page_id");
my @photos = $ua->find_all_links(url_regex => qr{/photos/.*/\d+/
+},);
print join("\n* ", "Page $page_id:", map { $_->url() } @photos),
"\n";
} ## end for my $page_id (1 .. $npages)
return;
} ## end sub search
__END__
=head1 NAME
zooomr - a simple command-line interface for Zooomr
=head1 VERSION
shell$ zooomr --version
=head1 USAGE
zooomr [--usage] [--help] [--man] [--version]
zooomr <command> [<command-specific options>]
[--check|-c] [--config|-C <filename>]
[--cookie|--cookie-file|-k <filename>] [--debug|-D]
[--dump|-d] [--login] [--password|-p password]
[--proxy | -P <proxy>] [--summary|--summary-file|-s <filenam
+e>]
[--tag|-t tag1 [--tag|-t tag2 [...]]] [--username|-u <userna
+me>]
command: add
[--backoff <time>] [--family]
[--files|--files-from|-f <filename>]
[--friends] [--max-retry|-m <retries>] [--pause <time>]
[--private] [--public] [--resize <max-size>]
=head1 EXAMPLES
shell$ zooomr add -u pippo@example.com -p pluto foto*.jpg
# Let the program ask you for the password, so that you don't have
# to show it on the screen
shell$ zooomr add -u pippo@example.com photo*.jpg
# Add a tag to the new upload
shell$ zooomr add -u pippo@example.com --tag awesome photo*.jpg
# Add two tags
shell$ zooomr add -u pippo@example.com -t awesome -t good photo*.jp
+g
=head1 DESCRIPTION
This script lets you interact with your Zooomr account (see
L<http://www.zooomr.com/>). You have to provide your account details,
tell it what you want to do and voil�, you're done. At the mome
+nt, the
following commands are recognised:
=over
=item *
B<add> one or more photo.
=back
=head2 Configuration File
You can put any configuration described in the L<OPTIONS> into a
configuration file. By default, the C<~/.zooomer> file is looked for
and loaded if exists.
The file format is quite easy: just put the configuration name, an
equal sign and the value you want to set. For boolean settings like
C<family> or C<public> use C<0> for false and C<1> for true.
Example:
username = pippo@example.com
password = pluto
private = 1
You can put comments and all the stuff, see L<Config::Tiny> for detail
+s.
Configurations on the command line override those found in the
configuration file. A notable exception to this rule is represented
by the tags, that are always added; this means that the tags in the
configuration file will be B<always> applied.
=head2 Logging in Zooomr
For operations that require authentication with Zooomr, like adding ne
+w
photos in your photostream, you can use two different methods:
=over
=item B<explicit>
in this case, you have to provide your B<username>/B<password> details
+,
either setting them in the configuration file, or passing them on the
command line, or passing the B<username> and waiting until prompted fo
+r
the B<password>. This method will not work for OpenID registered users
+.
=item B<cookie>
in this case, you rely on a previous authentication performed in the
browser (currently only Netscape and its descendants Mozilla/Firefox
are supported), and use cookies set by the browser. The mechanism is
simple: open the browser and log into Zooomr. Then find the cookies
file, and pass the path to this file with the C<--cookie> option (see
below).
For example, in my Linux system I have a C<.mozilla> subdirectory in
my home directory; to locate my cookies file, I would do the following
+:
shell$ find "$HOME/.mozilla" -name 'cookies.txt'
B<NOTE: OpenID users MUST use this method>. Users with normal
B<username>/B<password> pair can choose to use this method or the
B<explicit> one explained above.
=back
=head1 OPTIONS
Available options depend on the required action. In particular, you
have:
=over
=item B<* meta-options>
options that deal with the B<zooomr> script itself;
=item B<* common options>
options that are common to all actions, e.g. the account details
=item B<* per-action options>
options that are specific to a given required action.
=back
=head2 Meta-Options
These options don't really fire the script, but deal with the script
itself.
=over
=item --help
print a somewhat more verbose help, showing usage, this description of
the options and some examples from the synopsis.
=item --man
print out the full documentation for the script.
=item --usage
print a concise usage line and exit.
=item --version
print the version of the script.
=back
=head2 Common Options
These options are common to all commands.
=over
=item --check | -c
check options, print them and exit.
=item --config | -C <filename>
set name of configuration file to use, defaults to C<~/.zooomer>.
=item --cookie | --cookie-file | -k <filename>
set the file name for the cookie file to use. You can use this to
login in Zooomr with your browser (currently only
Netscape/Mozilla/Firefox are supported) and then use the browser's
cookies to access Zooomr with this script. In this case, it's not
necessary to provide a B<username>/B<password> pair, but you have
to ensure that you're logged in Zooomr with the browser.
OpenID users MUST use this method.
=item --debug | -D
set the debug level. Use one of the following:
=over
=item -
DEBUG
=item -
INFO
=item -
WARN
=item -
ERROR
=item -
FATAL
=back
=item --dump | -d
dump the entire configuration. Note that this will show your password
in clear in the screen, so use judiciously.
=item --login
this is a flag parameter to indicate that a login should be performed.
It is set by default, so you don't have to bother with it; in case you
want to I<disable> it, you can pass C<--no-login>.
=item --password | --pass | -p <password>
provide a password on the command line. If you don't set a password,
you will be asked one and you'll be able to type it without the risk
to display it explicitly (asterisks are written instead of the actual
keys pressed).
=item --proxy | -P <proxy>
set the HTTP proxy in use. Note that you can also set the environment
variable C<http_proxy>.
=item --summary | --summary-file | -s <filename>
save a summary of the upload into the given filename. The file will
contain a line for each file, containing the photo file name, a
C<TAB> and the URI where the photo is available in Zooomr.
Note that this file is written only at the end of the upload session.
If you break the upload script you won't get any file. This may change
in the future.
=item --tag | -t <tag>
add a tag to the tag list (aka I<labels>). You can set multiple tags
by using this parameter multiple times.
=item --username | --user | -u <username>
provide your account details. As of November 2007, this is an email
address. This parameter is mandatory.
=back
=head2 Action: I<add>
Action B<add> supports the options in the following list. Moreover, yo
+u
have to provide a list of filenames of photos that you want to upload.
=over
=item --backoff <time>
pause between two consecutive attempts to upload the same file. This v
+alue
is doubled every new attempt, and reset for each new file.
=item --family
set photos as private and accessible by family.
=item --files | --files-from | -f <filename>
get filenames of photos to upload from specified argument, e.g.:
shell$ ls | grep 'jpg$' > lista-jpg.text
shell$ zooomr add -u pippo@example.com -f lista-jpg-text
=item --friends
set photos as private and accessible by friends.
=item --max-retry | -m <retries>
set the maximum number of re-tries for an upload. Setting it to 0 mean
+s
that only one single attempt (i.e. the first) is tried.
=item --pause <time>
pause between the upload of two consecutive files, in second. Defaults
to 5 seconds.
=item --private
set photos as private.
=item --public
set photos as public.
=item --resize <max-size>
set a maximum dimension to which the uploaded photo must comply. If ei
+ther
width or height are greater than the provided value, the photo will be
resized in order for both to comply. The resize is done preserving the
aspect ratio.
You will need L<Image::Magick> to do this.
=back
A C<public> permission on the command line overrides any other
privacy configuration, either on the configuration file or on the
command line itself. On the other hand, stricter privacy configuration
+s
in the file override a public configuration in the file itself. While
it may seem counter-intuitive, you should probably avoid trying to
give such contradictive commands, and just make peace with your
brain.
The retry scheme tries to cope with some issues in the B<Zooomr> websi
+te.
Up to 1 photo out of 6 isn't actually received by the system, whatever
the result of the upload; for this reason, we try to see if the newly
uploaded photo is in the response page, which I<should> mean that
the upload was successful. The retry mechanism is ruled by options
C<backoff> and C<max-retry>.
Last, but not least, a pause can be inserted between two consecutive
files, in order to limit server CPU usage.
=head1 DIAGNOSTICS
To date, every error terminates the script. The error message should b
+e
explicit enough that you don't need more explainations (most of them
are provided by L<WWW::Mechanize>, so don't blame me).
=head1 CONFIGURATION AND ENVIRONMENT
zooomr relies on no environment variables.
By default, a configuration file is searched in C<~/.zoomer>. See
L<Configuration file> in L<DESCRIPTION> for details, and see
L<OPTIONS> for allowed parameters.
=head1 DEPENDENCIES
All stuff you can find on CPAN:
=over
=item L<version>
=item L<WWW::Mechanize> (the real star)
=item L<IO::Prompt>
=item L<Log::Log4perl>
=item L<Config::Tiny>
=item L<Path::Class>
=item L<List::MoreUtils>
=item L<Image::Magick>
but only if you want to use the C<resize> options in C<add>.
=back
=head1 BUGS AND LIMITATIONS
No bugs have been reported.
Please report any bugs or feature requests through http://rt.cpan.org/
The only supported action so far is B<add>, which is pretty little. To
be true, a B<search> action is currently implemented but still not
documented, mainly due to some residual decisions about the output
of the action.
The summary file (option C<--summary>) is written only after the uploa
+d
process completion for all files, so will be missing if anything
interrupts the upload itself.
=head1 AUTHOR
Flavio Poletti C<flavio@polettix.it>
=head1 LICENCE AND COPYRIGHT
Copyright (c) 2006, Flavio Poletti C<flavio@polettix.it>. All rights r
+eserved.
This script is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L<perlartistic>
and L<perlgpl>.
Questo script � software libero: potete ridistribuirlo e/o
modificarlo negli stessi termini di Perl stesso. Vedete anche
L<perlartistic> e L<perlgpl>.
=head1 DISCLAIMER OF WARRANTY
BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WH
+EN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. TH
+E
ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
NECESSARY SERVICING, REPAIR, OR CORRECTION.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
=head1 NEGAZIONE DELLA GARANZIA
Poiché questo software viene dato con una licenza gratuita, non
c'è alcuna garanzia associata ad esso, ai fini e per quanto permesso
dalle leggi applicabili. A meno di quanto possa essere specificato
altrove, il proprietario e detentore del copyright fornisce questo
software "così com'è" senza garanzia di alcun tipo, sia essa espressa
o implicita, includendo fra l'altro (senza però limitarsi a questo)
eventuali garanzie implicite di commerciabilità e adeguatezza per
uno scopo particolare. L'intero rischio riguardo alla qualità ed
alle prestazioni di questo software rimane a voi. Se il software
dovesse dimostrarsi difettoso, vi assumete tutte le responsabilità
ed i costi per tutti i necessari servizi, riparazioni o correzioni.
In nessun caso, a meno che ciò non sia richiesto dalle leggi vigenti
o sia regolato da un accordo scritto, alcuno dei detentori del diritto
di copyright, o qualunque altra parte che possa modificare, o redistri
+buire
questo software così come consentito dalla licenza di cui sopra, potrò
essere considerato responsabile nei vostri confronti per danni, ivi
inclusi danni generali, speciali, incidentali o conseguenziali, deriva
+nti
dall'utilizzo o dall'incapacità di utilizzo di questo software. Ciò
include, a puro titolo di esempio e senza limitarsi ad essi, la perdit
+a
di dati, l'alterazione involontaria o indesiderata di dati, le perdite
sostenute da voi o da terze parti o un fallimento del software ad
operare con un qualsivoglia altro software. Tale negazione di garanzia
rimane in essere anche se i dententori del copyright, o qualsiasi altr
+a
parte, sono stati avvisati della possibilità di tali danneggiamenti.
Se decidete di utilizzare questo software, lo fate a vostro rischio
e pericolo. Se pensate che i termini di questa negazione di garanzia
non si confacciano alle vostre esigenze, o al vostro modo di
considerare un software, o ancora al modo in cui avete sempre trattato
software di terze parti, non usatelo. Se lo usate, accettate espressam
+ente
questa negazione di garanzia e la piena responsabilità per qualsiasi
tipo di danno, di qualsiasi natura, possa derivarne.
=cut
|