summaryrefslogtreecommitdiffstats
path: root/Bugzilla/Update.pm
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla/Update.pm')
-rw-r--r--Bugzilla/Update.pm196
1 files changed, 196 insertions, 0 deletions
diff --git a/Bugzilla/Update.pm b/Bugzilla/Update.pm
new file mode 100644
index 000000000..a672bd8bb
--- /dev/null
+++ b/Bugzilla/Update.pm
@@ -0,0 +1,196 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# Contributor(s): Frédéric Buclin <LpSolit@gmail.com>
+
+package Bugzilla::Update;
+
+use strict;
+
+use Bugzilla::Config qw($datadir);
+
+use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml';
+use constant LOCAL_FILE => "/bugzilla-update.xml"; # Relative to $datadir.
+use constant TIME_INTERVAL => 604800; # Default is one week, in seconds.
+use constant TIMEOUT => 5; # Number of seconds before timeout.
+
+# Look for new releases and notify logged in administrators about them.
+sub get_notifications {
+ return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled');
+
+ # If the XML::Twig module is missing, we won't be able to parse
+ # the XML file. So there is no need to go further.
+ eval("require XML::Twig");
+ return if $@;
+
+ my $local_file = $datadir . LOCAL_FILE;
+ # Update the local XML file if this one doesn't exist or if
+ # the last modification time (stat[9]) is older than TIME_INTERVAL.
+ if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) {
+ # Are we sure we didn't try to refresh this file already
+ # but we failed because we cannot modify its timestamp?
+ my $can_alter = 1;
+ if (-e $local_file) {
+ # Try to alter its last modification time.
+ $can_alter = utime(undef, undef, $local_file);
+ }
+ if ($can_alter) {
+ _synchronize_data();
+ }
+ else {
+ return {'error' => 'no_update', 'xml_file' => $local_file};
+ }
+ }
+
+ # If we cannot access the local XML file, ignore it.
+ return {'error' => 'no_access', 'xml_file' => $local_file} unless (-r $local_file);
+
+ my $twig = XML::Twig->new();
+ $twig->safe_parsefile($local_file);
+ # If the XML file is invalid, return.
+ return {'error' => 'corrupted', 'xml_file' => $local_file} if $@;
+ my $root = $twig->root;
+
+ my @releases;
+ foreach my $branch ($root->children('branch')) {
+ my $release = {
+ 'branch_ver' => $branch->{'att'}->{'id'},
+ 'latest_ver' => $branch->{'att'}->{'vid'},
+ 'status' => $branch->{'att'}->{'status'},
+ 'url' => $branch->{'att'}->{'url'},
+ 'date' => $branch->{'att'}->{'date'}
+ };
+ push(@releases, $release);
+ }
+
+ # On which branch is the current installation running?
+ my @current_version =
+ ($Bugzilla::Config::VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
+
+ my @release;
+ if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') {
+ @release = grep {$_->{'status'} eq 'development'} @releases;
+ }
+ elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') {
+ @release = grep {$_->{'status'} eq 'stable'} @releases;
+ }
+ elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') {
+ # We want the latest stable version for the current branch.
+ # If we are running a development snapshot, we won't match anything.
+ my $branch_version = $current_version[0] . '.' . $current_version[1];
+
+ # We do a string comparison instead of a numerical one, because
+ # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older).
+ @release = grep {$_->{'branch_ver'} eq $branch_version} @releases;
+
+ # If the branch is now closed, we should strongly suggest
+ # to upgrade to the latest stable release available.
+ if (scalar(@release) && $release[0]->{'status'} eq 'closed') {
+ @release = grep {$_->{'status'} eq 'stable'} @releases;
+ return {'data' => $release[0], 'deprecated' => $branch_version};
+ }
+ }
+ else {
+ # Unknown parameter.
+ return {'error' => 'unknown_parameter'};
+ }
+
+ # Return if no new release is available.
+ return unless scalar(@release);
+
+ # Only notify the administrator if the latest version available
+ # is newer than the current one.
+ my @new_version =
+ ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
+
+ # We convert release candidates 'rc' to integers (rc ? 0 : 1) in order
+ # to compare versions easily.
+ $current_version[2] = ($current_version[2] && $current_version[2] eq 'rc') ? 0 : 1;
+ $new_version[2] = ($new_version[2] && $new_version[2] eq 'rc') ? 0 : 1;
+
+ my $is_newer = _compare_versions(\@current_version, \@new_version);
+ return ($is_newer == 1) ? {'data' => $release[0]} : undef;
+}
+
+sub _synchronize_data {
+ eval("require LWP::UserAgent");
+ return if $@;
+
+ my $local_file = $datadir . LOCAL_FILE;
+
+ my $ua = LWP::UserAgent->new();
+ $ua->timeout(TIMEOUT);
+ $ua->protocols_allowed(['http', 'https']);
+ $ua->mirror(REMOTE_FILE, $local_file);
+
+ # $ua->mirror() forces the modification time of the local XML file
+ # to match the modification time of the remote one.
+ # So we have to update it manually to reflect that a newer version
+ # of the file has effectively been requested. This will avoid
+ # any new download for the next TIME_INTERVAL.
+ utime(undef, undef, $local_file);
+}
+
+sub _compare_versions {
+ my ($old_ver, $new_ver) = @_;
+ while (scalar(@$old_ver) && scalar(@$new_ver)) {
+ my $old = shift(@$old_ver);
+ my $new = shift(@$new_ver);
+ return $new <=> $old if ($new <=> $old);
+ }
+ return scalar(@$new_ver) <=> scalar(@$old_ver);
+
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Update - Update routines for Bugzilla
+
+=head1 SYNOPSIS
+
+ use Bugzilla::Update;
+
+ # Get information about new releases
+ my $new_release = Bugzilla::Update::get_notifications();
+
+=head1 DESCRIPTION
+
+This module contains all required routines to notify you
+about new releases. It downloads an XML file from bugzilla.org
+and parses it, in order to display information based on your
+preferences. Absolutely no information about the Bugzilla version
+you are running is sent to bugzilla.org.
+
+=head1 FUNCTIONS
+
+=over
+
+=item C<get_notifications()>
+
+ Description: This function informs you about new releases, if any.
+
+ Params: None.
+
+ Returns: On success, a reference to a hash with data about
+ new releases, if any.
+ On failure, a reference to a hash with the reason
+ of the failure and the name of the unusable XML file.
+
+=back
+
+=cut