git-remote-mediawiki: split get_mw_pages into smaller functions
[git.git] / contrib / mw-to-git / git-remote-mediawiki
index c18bfa1f1515a8edb27c2d468a2982860a561939..361dbb1b28569a98e919eeb19d6353130e64fdcb 100755 (executable)
 use strict;
 use MediaWiki::API;
 use DateTime::Format::ISO8601;
-use encoding 'utf8';
 
-# use encoding 'utf8' doesn't change STDERROR
-# but we're going to output UTF-8 filenames to STDERR
+# By default, use UTF-8 to communicate with Git and the user
 binmode STDERR, ":utf8";
+binmode STDOUT, ":utf8";
 
 use URI::Escape;
+use IPC::Open2;
+
 use warnings;
 
 # Mediawiki filenames can contain forward slashes. This variable decides by which pattern they should be replaced
@@ -151,32 +152,170 @@ while (<STDIN>) {
 
 ########################## Functions ##############################
 
+## credential API management (generic functions)
+
+sub credential_from_url {
+       my $url = shift;
+       my $parsed = URI->new($url);
+       my %credential;
+
+       if ($parsed->scheme) {
+               $credential{protocol} = $parsed->scheme;
+       }
+       if ($parsed->host) {
+               $credential{host} = $parsed->host;
+       }
+       if ($parsed->path) {
+               $credential{path} = $parsed->path;
+       }
+       if ($parsed->userinfo) {
+               if ($parsed->userinfo =~ /([^:]*):(.*)/) {
+                       $credential{username} = $1;
+                       $credential{password} = $2;
+               } else {
+                       $credential{username} = $parsed->userinfo;
+               }
+       }
+
+       return %credential;
+}
+
+sub credential_read {
+       my %credential;
+       my $reader = shift;
+       my $op = shift;
+       while (<$reader>) {
+               my ($key, $value) = /([^=]*)=(.*)/;
+               if (not defined $key) {
+                       die "ERROR receiving response from git credential $op:\n$_\n";
+               }
+               $credential{$key} = $value;
+       }
+       return %credential;
+}
+
+sub credential_write {
+       my $credential = shift;
+       my $writer = shift;
+       while (my ($key, $value) = each(%$credential) ) {
+               if ($value) {
+                       print $writer "$key=$value\n";
+               }
+       }
+}
+
+sub credential_run {
+       my $op = shift;
+       my $credential = shift;
+       my $pid = open2(my $reader, my $writer, "git credential $op");
+       credential_write($credential, $writer);
+       print $writer "\n";
+       close($writer);
+
+       if ($op eq "fill") {
+               %$credential = credential_read($reader, $op);
+       } else {
+               if (<$reader>) {
+                       die "ERROR while running git credential $op:\n$_";
+               }
+       }
+       close($reader);
+       waitpid($pid, 0);
+       my $child_exit_status = $? >> 8;
+       if ($child_exit_status != 0) {
+               die "'git credential $op' failed with code $child_exit_status.";
+       }
+}
+
 # MediaWiki API instance, created lazily.
 my $mediawiki;
 
 sub mw_connect_maybe {
        if ($mediawiki) {
-           return;
+               return;
        }
        $mediawiki = MediaWiki::API->new;
        $mediawiki->{config}->{api_url} = "$url/api.php";
        if ($wiki_login) {
-               if (!$mediawiki->login({
-                       lgname => $wiki_login,
-                       lgpassword => $wiki_passwd,
-                       lgdomain => $wiki_domain,
-               })) {
-                       print STDERR "Failed to log in mediawiki user \"$wiki_login\" on $url\n";
-                       print STDERR "(error " .
-                           $mediawiki->{error}->{code} . ': ' .
-                           $mediawiki->{error}->{details} . ")\n";
-                       exit 1;
+               my %credential = credential_from_url($url);
+               $credential{username} = $wiki_login;
+               $credential{password} = $wiki_passwd;
+               credential_run("fill", \%credential);
+               my $request = {lgname => $credential{username},
+                              lgpassword => $credential{password},
+                              lgdomain => $wiki_domain};
+               if ($mediawiki->login($request)) {
+                       credential_run("approve", \%credential);
+                       print STDERR "Logged in mediawiki user \"$credential{username}\".\n";
                } else {
-                       print STDERR "Logged in with user \"$wiki_login\".\n";
+                       print STDERR "Failed to log in mediawiki user \"$credential{username}\" on $url\n";
+                       print STDERR "  (error " .
+                               $mediawiki->{error}->{code} . ': ' .
+                               $mediawiki->{error}->{details} . ")\n";
+                       credential_run("reject", \%credential);
+                       exit 1;
+               }
+       }
+}
+
+## Functions for listing pages on the remote wiki
+sub get_mw_tracked_pages {
+       my $pages = shift;
+       my @some_pages = @tracked_pages;
+       while (@some_pages) {
+               my $last = 50;
+               if ($#some_pages < $last) {
+                       $last = $#some_pages;
                }
+               my @slice = @some_pages[0..$last];
+               get_mw_first_pages(\@slice, $pages);
+               @some_pages = @some_pages[51..$#some_pages];
        }
 }
 
+sub get_mw_tracked_categories {
+       my $pages = shift;
+       foreach my $category (@tracked_categories) {
+               if (index($category, ':') < 0) {
+                       # Mediawiki requires the Category
+                       # prefix, but let's not force the user
+                       # to specify it.
+                       $category = "Category:" . $category;
+               }
+               my $mw_pages = $mediawiki->list( {
+                       action => 'query',
+                       list => 'categorymembers',
+                       cmtitle => $category,
+                       cmlimit => 'max' } )
+                       || die $mediawiki->{error}->{code} . ': '
+                               . $mediawiki->{error}->{details};
+               foreach my $page (@{$mw_pages}) {
+                       $pages->{$page->{title}} = $page;
+               }
+       }
+}
+
+sub get_mw_all_pages {
+       my $pages = shift;
+       # No user-provided list, get the list of pages from the API.
+       my $mw_pages = $mediawiki->list({
+               action => 'query',
+               list => 'allpages',
+               aplimit => 'max'
+       });
+       if (!defined($mw_pages)) {
+               print STDERR "fatal: could not get the list of wiki pages.\n";
+               print STDERR "fatal: '$url' does not appear to be a mediawiki\n";
+               print STDERR "fatal: make sure '$url/api.php' is a valid page.\n";
+               exit 1;
+       }
+       foreach my $page (@{$mw_pages}) {
+               $pages->{$page->{title}} = $page;
+       }
+}
+
+# queries the wiki for a set of pages. Meant to be used within a loop
+# querying the wiki for slices of page list.
 sub get_mw_first_pages {
        my $some_pages = shift;
        my @some_pages = @{$some_pages};
@@ -205,6 +344,7 @@ sub get_mw_first_pages {
        }
 }
 
+# Get the list of pages to be fetched according to configuration.
 sub get_mw_pages {
        mw_connect_maybe();
 
@@ -214,61 +354,24 @@ sub get_mw_pages {
                $user_defined = 1;
                # The user provided a list of pages titles, but we
                # still need to query the API to get the page IDs.
-
-               my @some_pages = @tracked_pages;
-               while (@some_pages) {
-                       my $last = 50;
-                       if ($#some_pages < $last) {
-                               $last = $#some_pages;
-                       }
-                       my @slice = @some_pages[0..$last];
-                       get_mw_first_pages(\@slice, \%pages);
-                       @some_pages = @some_pages[51..$#some_pages];
-               }
+               get_mw_tracked_pages(\%pages);
        }
        if (@tracked_categories) {
                $user_defined = 1;
-               foreach my $category (@tracked_categories) {
-                       if (index($category, ':') < 0) {
-                               # Mediawiki requires the Category
-                               # prefix, but let's not force the user
-                               # to specify it.
-                               $category = "Category:" . $category;
-                       }
-                       my $mw_pages = $mediawiki->list( {
-                               action => 'query',
-                               list => 'categorymembers',
-                               cmtitle => $category,
-                               cmlimit => 'max' } )
-                           || die $mediawiki->{error}->{code} . ': ' . $mediawiki->{error}->{details};
-                       foreach my $page (@{$mw_pages}) {
-                               $pages{$page->{title}} = $page;
-                       }
-               }
+               get_mw_tracked_categories(\%pages);
        }
        if (!$user_defined) {
-               # No user-provided list, get the list of pages from
-               # the API.
-               my $mw_pages = $mediawiki->list({
-                       action => 'query',
-                       list => 'allpages',
-                       aplimit => 500,
-               });
-               if (!defined($mw_pages)) {
-                       print STDERR "fatal: could not get the list of wiki pages.\n";
-                       print STDERR "fatal: '$url' does not appear to be a mediawiki\n";
-                       print STDERR "fatal: make sure '$url/api.php' is a valid page.\n";
-                       exit 1;
-               }
-               foreach my $page (@{$mw_pages}) {
-                       $pages{$page->{title}} = $page;
-               }
+               get_mw_all_pages(\%pages);
        }
        return values(%pages);
 }
 
+# usage: $out = run_git("command args");
+#        $out = run_git("command args", "raw"); # don't interpret output as UTF-8.
 sub run_git {
-       open(my $git, "-|:encoding(UTF-8)", "git " . $_[0]);
+       my $args = shift;
+       my $encoding = (shift || "encoding(UTF-8)");
+       open(my $git, "-|:$encoding", "git " . $args);
        my $res = do { local $/; <$git> };
        close($git);
 
@@ -624,6 +727,63 @@ sub error_non_fast_forward {
        return 0;
 }
 
+sub mw_upload_file {
+       my $complete_file_name = shift;
+       my $new_sha1 = shift;
+       my $extension = shift;
+       my $file_deleted = shift;
+       my $summary = shift;
+       my $newrevid;
+       my $path = "File:" . $complete_file_name;
+       my %hashFiles = get_allowed_file_extensions();
+       if (!exists($hashFiles{$extension})) {
+               print STDERR "$complete_file_name is not a permitted file on this wiki.\n";
+               print STDERR "Check the configuration of file uploads in your mediawiki.\n";
+               return $newrevid;
+       }
+       # Deleting and uploading a file requires a priviledged user
+       if ($file_deleted) {
+               mw_connect_maybe();
+               my $query = {
+                       action => 'delete',
+                       title => $path,
+                       reason => $summary
+               };
+               if (!$mediawiki->edit($query)) {
+                       print STDERR "Failed to delete file on remote wiki\n";
+                       print STDERR "Check your permissions on the remote site. Error code:\n";
+                       print STDERR $mediawiki->{error}->{code} . ':' . $mediawiki->{error}->{details};
+                       exit 1;
+               }
+       } else {
+               # Don't let perl try to interpret file content as UTF-8 => use "raw"
+               my $content = run_git("cat-file blob $new_sha1", "raw");
+               if ($content ne "") {
+                       mw_connect_maybe();
+                       $mediawiki->{config}->{upload_url} =
+                               "$url/index.php/Special:Upload";
+                       $mediawiki->edit({
+                               action => 'upload',
+                               filename => $complete_file_name,
+                               comment => $summary,
+                               file => [undef,
+                                        $complete_file_name,
+                                        Content => $content],
+                               ignorewarnings => 1,
+                       }, {
+                               skip_encoding => 1
+                       } ) || die $mediawiki->{error}->{code} . ':'
+                                . $mediawiki->{error}->{details};
+                       my $last_file_page = $mediawiki->get_page({title => $path});
+                       $newrevid = $last_file_page->{revid};
+                       print STDERR "Pushed file: $new_sha1 - $complete_file_name.\n";
+               } else {
+                       print STDERR "Empty file $complete_file_name not pushed.\n";
+               }
+       }
+       return $newrevid;
+}
+
 sub mw_push_file {
        my $diff_info = shift;
        # $diff_info contains a string in this format:
@@ -636,7 +796,8 @@ sub mw_push_file {
        my $summary = shift;
        # MediaWiki revision number. Keep the previous one by default,
        # in case there's no edit to perform.
-       my $newrevid = shift;
+       my $oldrevid = shift;
+       my $newrevid;
 
        my $new_sha1 = $diff_info_split[3];
        my $old_sha1 = $diff_info_split[2];
@@ -644,9 +805,11 @@ sub mw_push_file {
        my $page_deleted = ($new_sha1 eq NULL_SHA1);
        $complete_file_name = mediawiki_clean_filename($complete_file_name);
 
-       if (substr($complete_file_name,-3) eq ".mw") {
-               my $title = substr($complete_file_name,0,-3);
-
+       my ($title, $extension) = $complete_file_name =~ /^(.*)\.([^\.]*)$/;
+       if (!defined($extension)) {
+               $extension = "";
+       }
+       if ($extension eq "mw") {
                my $file_content;
                if ($page_deleted) {
                        # Deleting a page usually requires
@@ -664,7 +827,7 @@ sub mw_push_file {
                        action => 'edit',
                        summary => $summary,
                        title => $title,
-                       basetimestamp => $basetimestamps{$newrevid},
+                       basetimestamp => $basetimestamps{$oldrevid},
                        text => mediawiki_clean($file_content, $page_created),
                                  }, {
                                          skip_encoding => 1 # Helps with names with accentuated characters
@@ -676,7 +839,7 @@ sub mw_push_file {
                                    $mediawiki->{error}->{code} .
                                    ' from mediwiki: ' . $mediawiki->{error}->{details} .
                                    ".\n";
-                               return ($newrevid, "non-fast-forward");
+                               return ($oldrevid, "non-fast-forward");
                        } else {
                                # Other errors. Shouldn't happen => just die()
                                die 'Fatal: Error ' .
@@ -687,8 +850,11 @@ sub mw_push_file {
                $newrevid = $result->{edit}->{newrevid};
                print STDERR "Pushed file: $new_sha1 - $title\n";
        } else {
-               print STDERR "$complete_file_name not a mediawiki file (Not pushable on this version of git-remote-mediawiki).\n"
+               $newrevid = mw_upload_file($complete_file_name, $new_sha1,
+                                          $extension, $page_deleted,
+                                          $summary);
        }
+       $newrevid = ($newrevid or $oldrevid);
        return ($newrevid, "ok");
 }
 
@@ -791,8 +957,8 @@ sub mw_push_revision {
                # TODO: we could detect rename, and encode them with a #redirect on the wiki.
                # TODO: for now, it's just a delete+add
                my @diff_info_list = split(/\0/, $diff_infos);
-               # Keep the first line of the commit message as mediawiki comment for the revision
-               my $commit_msg = (split(/\n/, run_git("show --pretty=format:\"%s\" $sha1_commit")))[0];
+               # Keep the subject line of the commit message as mediawiki comment for the revision
+               my $commit_msg = run_git("log --no-walk --format=\"%s\" $sha1_commit");
                chomp($commit_msg);
                # Push every blob
                while (@diff_info_list) {
@@ -825,3 +991,18 @@ sub mw_push_revision {
        print STDOUT "ok $remote\n";
        return 1;
 }
+
+sub get_allowed_file_extensions {
+       mw_connect_maybe();
+
+       my $query = {
+               action => 'query',
+               meta => 'siteinfo',
+               siprop => 'fileextensions'
+               };
+       my $result = $mediawiki->api($query);
+       my @file_extensions= map $_->{ext},@{$result->{query}->{fileextensions}};
+       my %hashFile = map {$_ => 1}@file_extensions;
+
+       return %hashFile;
+}