#!/usr/bin/perl
#
# applicaton/x-external-editor
# reference implementation of the helper application
#
# written by Erik M�ler - public domain
#
# User documentation: http://meta.wikimedia.org/wiki/Help:External_editors
# Technical documentation: http://meta.wikimedia.org/wiki/Help:External_editors/Tech
#
# To do: Edit conflicts
#
use Config::IniFiles; # Module for config files in .ini syntax
use LWP::UserAgent; # Web agent module for retrieving and posting HTTP data
use URI::Escape; # Urlencode functions
use Gtk2 '-init'; # Graphical user interface, requires GTK2 libraries
use Encode qw(encode); # UTF-8/iso8859-1 encoding
use HTML::Entities; # Encode or decode strings with HTML entities
# Load interface messages
initmsg();
# By default, config will be searched for in your Unix home directory
# (e.g. ~/.ee-helper/ee.ini). Change path of the configuration file if needed!
#
# Under Windows, set to something like
# $cfgfile='c:\ee\ee.ini';
# (note single quotes!)
$cfgfile=$ENV{HOME}."/systeme/ee.ini";
$cfgfile=getunixpath($cfgfile);
$DEBUG=0;
$NOGUIERRORS=0;
$LANGUAGE="fr";
# Read config
my $cfg = new Config::IniFiles( -file => $cfgfile ) or vdie (_("noinifile",$cfgfile));
# Treat spaces as part of input filename
my $args=join(" ",@ARGV);
# Where do we store our files?
my $tempdir=$cfg->val("Settings","Temp Path") or vdie (_("notemppath",$cfgfile));
# Remove slash at the end of the directory name, if existing
$/="/";
chomp($tempdir);
$/="\\";
chomp($tempdir);
my $unixtempdir=getunixpath($tempdir);
if($DEBUG) {
# Make a copy of the control (input) file in the log
open(DEBUGLOG,">$unixtempdir/debug.log");
open(INPUT,"<$args");
$/=undef; # slurp mode
while(<INPUT>) {
$inputfile=$_;
}
print DEBUGLOG $inputfile;
close(INPUT);
}
# Read the control file
if(-e $args) {
$input = new Config::IniFiles( -file => $args );
} else {
vdie (_("nocontrolfile"));
}
# Initialize the browser as Firefox 1.0 with new cookie jar
$browser=LWP::UserAgent->new();
$browser->cookie_jar( {} );
@ns_headers = (
'User-Agent' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7) Gecko/20041107 Firefox/1.0',
'Accept' => 'image/gif, image/x-xbitmap, image/jpeg,
image/pjpeg, image/png, */*',
'Accept-Charset' => 'iso-8859-1,*,utf-8',
'Accept-Language' => 'en-US',
);
# Obtain parameters from control file
$special=$input->val("Process","Special namespace");
$fileurl=$input->val("File","URL");
$type=$input->val("Process","Type");
$script=$input->val("Process","Script");
$server=$input->val("Process","Server");
$path=$input->val("Process","Path");
$login_url=$script."?title=$special:Userlogin&action=submitlogin";
$ext=$input->val("File","Extension");
if($special eq "") { $special="Special"; };
# Edit file: change an image, sound etc. in an external app
# Edit text: change a regular text file
# In both cases, we need to construct the relevant URLs
# to fetch and post the data.
if($type eq "Edit file") {
$filename=substr($fileurl,rindex($fileurl,"/")+1);
# Image: is canonical namespace name, should always work
$view_url=$script."?title=Image:$filename";
$upload_url=$script."?title=$special:Upload";
} elsif($type eq "Edit text") {
$fileurl=~m|\?title=(.*?)\&action=|i;
$pagetitle=$1;
$filename=uri_unescape($pagetitle);
# substitute illegal or special characters
$filename =~ s/:/__/g; # : - illegal on Windows
$filename =~ s|/|__|g; # / - path character under Unix and others
# potential shell metacharacters:
$filename =~ s/[\[\]\{\}\(\)~!#\$\^\&\*;'"<>\?]/__/g;
$filename=$filename.".wiki";
$edit_url=$script."?title=$pagetitle&action=submit";
$view_url=$script."?title=$pagetitle";
} elsif($type eq "Diff text") {
$secondurl=$input->val("File 2","URL");
if(!$secondurl) {
vdie (_("twofordiff"));
}
$diffcommand=$cfg->val("Settings","Diff");
if(!$diffcommand) {
vdie (_("nodifftool"));
}
} else {
# Nothing we know!
vdie (_("unknownprocess"));
}
# Obtain settings from config file
$previewclient=$cfg->val("Settings","Browser");
$browseaftersave=$cfg->val("Settings","Browse after save");
# The config file can contain definitions for any number
# of site. Each one of them should have an "URL match", which is
# a simple string expression that needs to be part of the
# URL in the control file in order for it to be recognized
# as that site. Using this methodology, we can define usernames
# and passwords for sites relatively easily.
#
# Here we try to match the URL in the control file against the
# URL matches in all sections to determine the username and
# password.
#
@sections=$cfg->Sections();
foreach $section(@sections) {
if($search=$cfg->val($section,"URL match")) {
if(index($fileurl,$search)>=0) {
$username=$cfg->val($section,"Username");
$password=$cfg->val($section,"Password");
}
}
}
# Log into server
# Note that we also log in for diffs, as the raw text might only be available
# to logged in users (depending on the wiki security settings), and we may want
# to offer GUI-based rollback functionality later
$response=$browser->post($login_url,@ns_headers,
Content=>[wpName=>$username,wpPassword=>$password,wpRemember=>"1",wpLoginAttempt=>"Log in"]);
# We expect a redirect after successful login
if($response->code!=302 && !$ignore_login_error) {
vdie (_("loginfailed",$login_url,$username,$password));
}
$response=$browser->get($fileurl,@ns_headers);
if($type eq "Edit file") {
open(OUTPUT,">$unixtempdir/".$filename);
binmode(OUTPUT);
select OUTPUT; $|=1; select STDOUT;
print OUTPUT $response->content;
close(OUTPUT);
}elsif($type eq "Edit text") {
# Do we need to convert UTF-8 into ISO 8859-1?
if($cfg->val("Settings","Transcode UTF-8") eq "true") {
$transcode=1;
}
# MediaWiki 1.4+ uses edit tokens, we need to get one
# before we can submit edits. So instead of action=raw, we use
# action=edit, and get the token as well as the text of the page
# we want to edit in one go.
$ct=$response->header('Content-Type');
$editpage=$response->content;
$editpage=~m|<input type='hidden' value="(.*?)" name="wpEditToken" />|i;
$token=$1;
$editpage=~m|<textarea.*?name="wpTextbox1".*?>(.*?)</textarea>|is;
$text=$1;
$editpage=~m|<input type='hidden' value="(.*?)" name="wpEdittime" />|i;
$time=$1;
# Do we need to convert ..?
if($ct=~m/charset=utf-8/i) {
$is_utf8=1;
}
# ..if so, do it.
if($is_utf8 && $transcode) {
Encode::from_to($text,'utf8','iso-8859-1');
}
# decode HTML entities
HTML::Entities::decode($text);
# Flush the raw text of the page to the disk
open(OUTPUT,">$unixtempdir/".$filename);
select OUTPUT; $|=1; select STDOUT;
print OUTPUT $text;
close(OUTPUT);
}
# Search for extension-associated application
@extensionlists=$cfg->Parameters("Editors");
foreach $extensionlist(@extensionlists) {
@exts=split(",",$extensionlist);
foreach $extensionfromlist(@exts) {
if ($extensionfromlist eq $ext) {
$app=$cfg->val("Editors",$extensionlist);
}
}
}
# In most cases, we'll want to run the GUI for managing saves & previews,
# and run the external editor application.
if($type ne "Diff text") {
if($^O eq "MSWin32") {
$appstring="$app $tempdir\\$filename";
} else {
$appstring="$app $tempdir/$filename";
}
$cid=fork();
if(!$cid) {
exec($appstring);
}
makegui();
} else {
# For external diffs, we need to create two temporary files.
$response1=$browser->get($fileurl,@ns_headers);
$response2=$browser->get($secondurl,@ns_headers);
open(DIFF1, ">$unixtempdir/diff-1.txt");
select DIFF1; $|=1; select STDOUT;
open(DIFF2, ">$unixtempdir/diff-2.txt");
select DIFF2; $|=1; select STDOUT;
print DIFF1 $response1->content;
print DIFF2 $response2->content;
close(DIFF1);
close(DIFF2);
if($^O eq "MSWin32") {
$appstring="$diffcommand $tempdir\\diff-1.txt $tempdir\\diff-2.txt";
} else {
$appstring="$diffcommand $tempdir/diff-1.txt $tempdir/diff-2.txt";
}
system($appstring);
}
# Create the GTK2 graphical user interface
# It should look like this:
# _______________________________________________
# | Summary: ____________________________________ |
# | |
# | [Save] [Save & Cont.] [Preview] [Cancel] |
# |_______________________________________________|
#
# Save: Send data to the server and quit ee.pl
# Save & cont.: Send data to the server, keep GUI open for future saves
# Preview: Create local preview file and view it in the browser (for text)
# Cancel: Quit ee.pl
#
sub makegui {
$title_label = Gtk2::Label->new($filename);
$vbox = Gtk2::VBox->new;
$hbox = Gtk2::HBox->new;
$label = Gtk2::Label->new(_("summary"));
$entry = Gtk2::Entry->new;
$hbox->pack_start_defaults($label);
$hbox->pack_start_defaults($entry);
$hbox2 = Gtk2::HBox->new;
$savebutton = Gtk2::Button->new(_("save"));
$savecontbutton = Gtk2::Button->new(_("savecont"));
$previewbutton = Gtk2::Button->new(_("preview"));
$cancelbutton = Gtk2::Button->new(_("cancel"));
$hbox2->pack_start_defaults($savebutton);
$hbox2->pack_start_defaults($savecontbutton);
$hbox2->pack_start_defaults($previewbutton);
$hbox2->pack_start_defaults($cancelbutton);
$vbox->pack_start_defaults($title_label);
$vbox->pack_start_defaults($hbox);
$vbox->pack_start_defaults($hbox2);
if($type ne "Edit file") {
$hbox3 = Gtk2::HBox->new;
$minoreditcheck = Gtk2::CheckButton->new_with_label(_("minoredit"));
$watchcheck = Gtk2::CheckButton->new_with_label(_("watchpage"));
$hbox3->pack_start_defaults($minoreditcheck);
$hbox3->pack_start_defaults($watchcheck);
$vbox->pack_start_defaults($hbox3);
if ($cfg->val("Settings","Minor edit default") =~ m/^(?:true|1)$/i) {
$minoreditcheck->set_active(1);
}
if ($cfg->val("Settings","Watch default") =~ m/^(?:true|1)$/i) {
$watchcheck->set_active(1);
}
}
# Set up window
$window = Gtk2::Window->new;
$window->set_title (_("entersummary"));
$window->signal_connect (delete_event => sub {Gtk2->main_quit});
$savebutton->signal_connect (clicked => \&save);
$savecontbutton->signal_connect ( clicked => \&savecont);
$previewbutton->signal_connect ( clicked => \&preview);
$cancelbutton->signal_connect (clicked => \&cancel);
if($type ne "Edit file") {
$minoreditcheck->get_active();
$watchcheck->get_active();
}
# Add vbox to window
$window->add($vbox);
$window->show_all;
Gtk2->main;
}
# Just let save function know that it shouldn't quit
sub savecont {
save("continue");
}
# Just let save function know that it shouldn't save
sub preview {
$preview=1;
save("continue");
}
sub save {
my $cont=shift;
my $summary=$entry->get_text();
my ($minorvar, $watchvar);
if($type ne "Edit file") {
$minorvar=$minoreditcheck->get_active();
$watchvar=$watchcheck->get_active();
}
# Spam the summary if room is available :-)
if(length($summary)<190) {
my $tosummary=_("usingexternal");
if(length($summary)>0) {
$tosummary=" [".$tosummary."]";
}
$summary.=$tosummary;
}
if($is_utf8) {
$summary=Encode::encode('utf8',$summary);
}
# Upload file back to the server and load URL in browser
if($type eq "Edit file") {
print $upload_url;
$response=$browser->post($upload_url,
@ns_headers,Content_Type=>'form-data',Content=>
[
wpUploadFile=>["$unixtempdir/".$filename],
wpUploadDescription=>$summary,
wpUploadAffirm=>"1",
wpUpload=>"Upload file",
wpIgnoreWarning=>"1"
]);
if($browseaftersave eq "true" && $previewclient && !$preview) {
$previewclient=~s/\$url/$view_url/i;
system(qq|$previewclient|);
$previewclient=$cfg->val("Settings","Browser");
}
# Save text back to the server & load in browser
} elsif($type eq "Edit text") {
open(TEXT,"<$unixtempdir/".$filename);
$/=undef;
while(<TEXT>) {
$text=$_;
}
close(TEXT);
if($is_utf8 && $transcode) {
Encode::from_to( $text, 'iso-8859-1','utf8');
}
@content = (
Content=>[
wpTextbox1=>$text,
wpSummary=>$summary,
wpEdittime=>$time,
wpEditToken=>$token]
);
$watchvar && push @{$content[1]}, (wpWatchthis=>"1");
$minorvar && push @{$content[1]}, (wpMinoredit=>"1");
$preview && push @{$content[1]}, (wpPreview=>"true");
if($preview) {
$response=$browser->post($edit_url,@ns_headers,
@content);
open(PREVIEW,">$unixtempdir/preview.html");
$preview=$response->content;
# Replace relative URLs with absolute ones
$preview=~s|<head>|<head>\n <base href="$server$path">|gi;
print PREVIEW $preview;
close(PREVIEW);
if($previewclient) {
$previewurl="file://$unixtempdir/preview.html";
$previewclient=~s/\$url/$previewurl/i;
system(qq|$previewclient|);
$previewclient=$cfg->val("Settings","Browser");
}
} else {
$response=$browser->post($edit_url,@ns_headers,@content);
}
if($browseaftersave eq "true" && $previewclient && !$preview) {
$previewclient=~s/\$url/$view_url/i;
system(qq|$previewclient|);
$previewclient=$cfg->val("Settings","Browser");
}
$preview=0;
}
if($cont ne "continue") {
Gtk2->main_quit;
exit 0;
}
}
sub cancel {
Gtk2->main_quit;
}
sub _{
my $message=shift;
@subst=@_;
my $suffix;
if($LANGUAGE ne "en") { $suffix = "_".$LANGUAGE; }
$msg=$messages{$message.$suffix};
foreach $substi(@subst) {
$msg=~s/____/$substi/s;
}
return $msg;
}
sub vdie {
my $errortext=shift;
if(!$NOGUIERRORS) {
errorbox($errortext);
}
die($errortext);
}
sub errorbox {
my $errortext=shift;
my $dialog = Gtk2::MessageDialog->new ($window,
[qw/modal destroy-with-parent/],
'error',
'ok',
$errortext);
$dialog->run;
$dialog->destroy;
}
sub getunixpath {
my $getpath=shift;
if($^O eq 'MSWin32') {
$getpath=~s|\\|/|gi;
}
return $getpath;
}
sub initmsg {
%messages=(
noinifile=>
"____ could not be found.
Please move the configuration file (ee.ini) there, or edit ee.pl
and point the variable \$cfgfile to the proper location.",
noinifile_de=>
"____ konnte nicht gefunden werden.
Bitte verschieben Sie die Konfigurations-Datei (ee.ini) dorthin,
oder bearbeiten Sie ee.pl und zeigen Sie die Variable \$cfgfile
auf die korrekte Datei.",
noinifile_fr=>
"____ est introuvable.
Deplacez-y le fichier de configuration (ee.ini), ou editez ee.pl
en indiquant dans la variable \$cfgfile le bon emplacement.",
notemppath=>
"No path for temporary files specified. Please edit ____
and add an entry like this:
[Settings]
Temp Path=/tmp\n",
notemppath_de=>
"Kein Pfad fr tempor�e Dateien festgelegt.
Bitte bearbeiten Sie ____
und fgen Sie einen Eintrag wie folgt ein:
[Settings]
Temp Path=/tmp\n",
notemppath_fr=>
"Aucune répertoire n'est indique pour les fichiers temporaires. Editez ____
et ajoutez la section suivante :
[Settings]
Temp Path=/tmp\n",
nocontrolfile=>
"No control file specified.
Syntax: perl ee.pl <control file>\n",
nocontrolfile_de=>
"Keine Kontrolldatei angegeben.
Syntax: perl ee.pl <Kontrolldatei>\n",
nocontrolfile_fr=>
"Aucun fichier de controle n'est specifie.
Syntaxe: perl ee.pl <fichier de controle>\n",
twofordiff=>
"Process is diff, but no second URL contained in control file\n",
twofordiff_de=>
"Dateien sollen verglichen werden, Kontrolldatei enth�t aber nur eine URL.\n",
twofordiff_fr=>
"Le processus est diff, mais li manque une seconde URL dans le fichier de controle.\n",
nodifftool=>
"Process is diff, but ee.ini does not contain a 'Diff=' definition line
in the [Settings] section where the diff tool is defined.\n",
nodifftool_de=>
"Dateien sollen verglichen werden, ee.ini enth�t aber keine
'Diff='-Zeile im Abschnitt Settings, in der das Diff-Werkzeug
definiert wird.\n",
nodifftool_fr=>
"Le processus est diff, mais ee.ini ne contient pas la ligne de definition 'Diff='
dans la section [Settings] ou l'outil diff doit etre defini.\n",
unknownprocess=>
"The process type defined in the input file (Type= in the [Process] section)
is not known to this implementation of the External Editor interface. Perhaps
you need to upgrade to a newer version?\n",
unknownprocess_de=>
"Der in der Kontrolldatei definierte Prozesstyp (Type= im Abschnitt [Process])
ist dieser Implementierung des application/x-external-editor-Interface nicht
bekannt. Vielleicht mssen Sie Ihre Version des Skripts aktualisieren.\n",
unknownprocess_fr=>
"Le type de processus defini dans le fichier d'entree (Type= dans la section
[Process]) est inconnu dans cette version de l'interface d'édition externe.
Peut-etre faudrait-il mettre a jour le script ee.pl ?\n",
loginfailed=>
"Could not login to
____
with username '____' and password '____'.
Make sure you have a definition for this website in your ee.ini, and that
the 'URL match=' part of the site definition contains a string that is part
of the URL above.\n",
loginfailed_de=>
"Anmeldung bei
____
gescheitert. Benutzername: ____ Passwort: ____
Stellen Sie sicher, dass Ihre ee.ini eine Definition fr diese Website
enth�t, und in der 'URL match='-Zeile ein Text steht, der Bestandteil der
obigen URL ist.\n",
loginfailed_fr=>
"Connexion impossible a
____
avec le nom d'utilisateur '____' et le mot de passe '____'.
Assurez-vous que le fichier ee.ini contient la definition du site internet,
et que la ligne 'URL match=' de la definition contient une chaine de caracteres
contenue dans l'adresse internet ci-dessus.\n",
summary=>
"Summary",
summary_de=>
"Zusammenfassung",
summary_fr=>
"Resume :",
save=>
"Save",
save_de=>
"Speichern",
save_fr=>
"Sauvegarder",
savecont=>
"Save and continue",
savecont_de=>
"Speichern und weiter",
savecont_fr=>
"Sauvegarder et poursuivre",
preview=>
"Preview",
preview_de=>
"Vorschau",
preview_fr=>
"Previsualisation",
cancel=>
"Cancel",
cancel_de=>
"Abbruch",
cancel_fr=>
"Annuler",
entersummary=>
"Enter edit summary",
entersummary_de=>
"Zusammenfassung eingeben",
entersummary_fr=>
"Interface d'edition Wiki",
minoredit=>
"Check as minor edit",
minoredit_de=>
"Kleine �derung",
minoredit_fr=>
"Modification mineure",
watchpage=>
"Watch this page",
watchpage_de=>
"Seite beobachten",
watchpage_fr=>
"Suivre cette page",
usingexternal=>
"using [[Help:External editors|an external editor]]",
usingexternal_de=>
"mit [[Hilfe:Externe Editoren|externem Editor]]",
usingexternal_fr=>
"avec [[m:Aide:%C3%89diteurs_externes|un editeur externe]]",
);
}