File Coverage

blib/lib/Socialtext/EditPage.pm
Criterion Covered Total %
statement 155 169 91.7
branch 32 42 76.1
condition 11 22 50.0
subroutine 18 18 100.0
pod 3 3 100.0
total 219 254 86.2


line stmt bran cond sub pod time code
1             package Socialtext::EditPage;
2 3     3   37952 use warnings;
  3         7  
  3         127  
3 3     3   16 use strict;
  3         5  
  3         94  
4 3     3   17 use Carp qw/croak/;
  3         6  
  3         181  
5 3     3   4026 use File::Temp;
  3         69833  
  3         242  
6 3     3   1480 use Socialtext::Resting::DefaultRester;
  3         12  
  3         116  
7 3     3   37 use Socialtext::Resting;
  3         5  
  3         62  
8 3     3   17 use JSON::XS;
  3         7  
  3         8020  
9              
10             =head1 NAME
11              
12             Socialtext::EditPage - Edit a wiki page using your favourite EDITOR.
13              
14             =cut
15              
16             our $VERSION = '0.04';
17              
18             =head1 SYNOPSIS
19              
20             Fetch a page, edit it, and then post it.
21              
22             use Socialtext::EditPage;
23              
24             # The rester is set with the server and workspace
25             my $rester = Socialtext::Resting->new(%opts);
26              
27             my $s = Socialtext::EditPage->new(rester => $rester);
28             $s->edit_page('Snakes on a Plane');
29              
30             =head1 FUNCTIONS
31              
32             =head2 new( %opts )
33              
34             Arguments:
35              
36             =over 4
37              
38             =item rester
39              
40             Users must provide a Socialtext::Resting object setup to use the desired
41             workspace and server.
42              
43             =item pull_includes
44              
45             If true, C wafls will be inlined into the page as extraclude
46             blocks.
47              
48             =back
49              
50             =cut
51              
52             sub new {
53 14     14 1 34285 my ($class, %opts) = @_;
54 14   33     64 $opts{rester} ||= Socialtext::Resting::DefaultRester->new(%opts);
55 14         53 my $self = { %opts };
56 14         46 bless $self, $class;
57 14         47 return $self;
58             }
59              
60             =head2 C
61              
62             This method will fetch the page content, and then run $EDITOR on the file.
63             After the file has been edited, it will be put back on the wiki server.
64              
65             Arguments:
66              
67             =over 4
68              
69             =item page
70              
71             The name of the page you wish to edit.
72              
73             =item callback
74              
75             If supplied, callback will be called after the page has been edited. This
76             function will be passed the edited content, and should return the content to
77             be put onto the server.
78              
79             =item summary_callback
80              
81             If supplied, callback will be called after the page has been edit. This
82             function should return the edit summary text for this edit, if desired.
83              
84             =item tags
85              
86             If supplied, these tags will be applied to the page after it is updated.
87              
88             =item output
89              
90             If supplied, the page will be saved to the given file instead of edited.
91             The page will not be uploaded to the server.
92              
93             =item template
94              
95             If specified, this page will be used as the template for a new page.
96              
97             =back
98              
99             =cut
100              
101             sub edit_page {
102 15     15 1 216 my $self = shift;
103 15         136 my %args = @_;
104 15         114 my $page = $self->{page} = delete $args{page};
105 15 50       53 croak "page is mandatory" unless $page;
106              
107 15         41 my $rester = $self->{rester};
108 15         69 my $content = $self->_get_page($page);
109              
110 15   100     171 my $tags = delete $args{tags} || [];
111 15 100       55 if ($args{template}) {
112 2 100       8 if ($rester->response->code eq '404') {
113 1         14 $content = $self->_get_page($args{template});
114             }
115             else {
116 1         183 print "Not using template '$args{template}' - page already "
117             . "exists.\n";
118             }
119 2         17 $rester->accept('text/plain');
120 2         8 my @tmpl_tags = grep { !/^template$/ } $rester->get_pagetags($args{template});
  1         8  
121 2         11 push @$tags, @tmpl_tags;
122             }
123              
124 15 50       45 if ($args{output}) {
125 0         0 _write_file($args{output}, $content);
126 0         0 print "Wrote $page content to $args{output}\n";
127 0         0 return;
128             }
129              
130 15         25 my $orig_content = $content;
131 15         22 my $edit_summary;
132 15         21 while (1) {
133 15         22 my $new_content = $content;
134 15         70 $new_content = $self->_pre_process_special_wafls($new_content);
135 15         99 $new_content = $self->_edit_content($new_content);
136              
137 15 100       10513 if ($orig_content eq $new_content) {
138 1         290 print "$page did not change.\n";
139 1         17 return;
140             }
141              
142 14 100       124 $new_content = $args{callback}->($new_content) if $args{callback};
143              
144 14         183 $new_content = $self->_process_special_wafls($new_content);
145              
146 14 100 33     82 $edit_summary ||= $args{summary_callback}->() if $args{summary_callback};
147              
148 14         77 eval {
149 14         72 $page =~ s#/#-#g; # cannot have /'s in the page name
150 14 100       517 $rester->put_page($page, {
151             content => $new_content,
152             date => scalar(gmtime),
153             ($edit_summary ? (edit_summary => $edit_summary) : ()),
154             }
155             );
156             };
157 14 100       86 last unless $@;
158 2 50       27 if ($@ =~ /412/) { # collision detected!
159 0         0 print "A collision was detected. I will merge the changes and "
160             . "re-open your editor.\nHit enter.\n";
161 0         0 sleep 2;
162 0         0 print "Merging...\n";
163 0         0 $orig_content = $self->_get_page($page);
164 0         0 my $updated_file = _write_file(undef, $orig_content);
165 0         0 my $orig_file = _write_file(undef, $content);
166 0         0 my $our_file = _write_file(undef, $new_content);
167              
168             # merge the content and re-edit
169             # XXX: STDERR is not redirected. Should use IPC::Run. However,
170             # it's nice to be able to create pages w/ quotes and other shell
171             # characters in their name.
172 0         0 system(qw(merge -L yours -L original -L), "new edit", $our_file,
173             $orig_file, $updated_file);
174              
175 0         0 $content = _read_file($our_file);
176             }
177             else {
178 2         24 $self->_handle_error($@, $page, $new_content);
179             }
180             }
181              
182 12 50       78 if ($tags) {
183 12 100       58 $tags = [$tags] unless ref($tags) eq 'ARRAY';
184 12         54 for my $tag (@$tags) {
185 5         1379 print "Putting page tag $tag on $page\n";
186 5         39 $rester->put_pagetag($page, $tag);
187             }
188             }
189              
190 12         2976 print "Updated page $page\n";
191             }
192              
193             =head2 C
194              
195             This method will retrieve a last of all pages tagged with the supplied
196             tag, and then open the latest one for edit.
197              
198             Arguments are passed through to edit_page(), accept for:
199              
200             =over 4
201              
202             =item tag
203              
204             The name of the tag you wish to edit.
205              
206             =back
207              
208             =cut
209              
210             sub edit_last_page {
211 1     1 1 19 my $self = shift;
212 1         7 my %opts = @_;
213              
214 1   33     6 my $tag = delete $opts{tag} || croak "tag is mandatory";
215 1         4 my $rester = $self->{rester};
216 1         8 $rester->accept('application/json');
217 1         6 my $pages = decode_json($rester->get_taggedpages($tag));
218 1 50       17 unless (@$pages) {
219 0         0 die "No pages found tagged '$tag'\n";
220             }
221 1         16 my @pages = sort { $b->{modified_time} <=> $a->{modified_time} }
  1         7  
222             @$pages;
223 1         6 my $newest_page = shift @pages;
224 1         154 print "Editing '$newest_page->{name}'\n";
225 1         10 $self->edit_page(page => $newest_page->{page_id}, %opts);
226             }
227              
228             sub _get_page {
229 18     18   30 my $self = shift;
230 18         39 my $page_name = shift;
231 18         41 my $rester = $self->{rester};
232 18         137 $rester->accept('text/x.socialtext-wiki');
233              
234 18         60 my $page = $rester->get_page($page_name);
235              
236 18 100       64 if ($self->{pull_includes}) {
237 3         23 while ($page =~ m/({include:?\s+\[([^\]]+)\]\s*}\n)/smg) {
238 2         6 my $included_page = $2;
239 2         86 my ($match_start, $match_size) = ($-[0], $+[0] - $-[0]);
240 2         246 print "Pulling include in [$page_name] - [$included_page]\n";
241 2         22 my $pulled_content = $self->_get_page($included_page);
242 2         8 chomp $pulled_content;
243 2         11 my $included_content = ".pulled-extraclude [$included_page]\n"
244             . "$pulled_content\n"
245             . ".pulled-extraclude\n";
246              
247 2         17 substr($page, $match_start, $match_size) = $included_content;
248             }
249             }
250              
251 18         44 return $page;
252             }
253              
254             sub _edit_content {
255 15     15   24 my $self = shift;
256 15         33 my $content = shift;
257              
258 15   100     73 my $workspace = $self->{rester}->workspace || '';
259 15         159 (my $page = $self->{page}) =~ s#/#_#g;
260 15         242 my $filename = File::Temp->new(
261             TEMPLATE => "$workspace.$page.XXXX",
262             SUFFIX => '.wiki'
263             );
264 15         10375 _write_file($filename, $content);
265 15   50     70 my $editor = $ENV{EDITOR} || '/usr/bin/vim';
266              
267 15         208109 system( $editor, $filename );
268              
269 15         806 return _read_file($filename);
270             }
271              
272             {
273             my @special_wafls = (
274             [ '.extraclude' => '.e-x-t-r-a-c-l-u-d-e' ],
275             [ '.pulled-extraclude' => '.extraclude', 'pre-only' ],
276             );
277              
278             sub _pre_process_special_wafls {
279 15     15   23 my $self = shift;
280 15         21 my $text = shift;
281              
282             # Escape special wafls
283 15         42 for my $w (@special_wafls) {
284 30         66 my $wafl = $w->[0];
285 30         60 my $expanded = $w->[1];
286 30         733 $text =~ s/\Q$wafl\E\b/$expanded/g;
287             }
288 15         44 return $text;
289             }
290              
291             sub _process_special_wafls {
292 14     14   34 my $self = shift;
293 14         30 my $text = shift;
294 14         55 my $rester = $self->{rester};
295              
296             my $included_content = sub {
297 6     6   46 my $type = lc shift;
298 6         40 my $name = shift;
299 6   50     36 my $newline = shift || '';
300              
301 6 100       56 if ($type eq 'clude') {
    50          
302 4         41 return "{include: [$name]}\n";
303             }
304             elsif ($type eq 'link') {
305 2         30 return "[$name]$newline";
306             }
307 0         0 die "Unknown extrathing: $type";
308 14         230 };
309              
310 14         165 while ($text =~ s/\.extra(clude|link)\s # $1 is title
311             \[([^\]]+)\] # $2 is [name]
312             (\n?) # $3 is extra newline
313             (.+?)
314             \.extra(?:clude|link)\n
315 6         53 /$included_content->($1, $2, $3)/ismex) {
316 6         160 my ($page, $new_content) = ($2, $4);
317 6         1423 print "Putting extraclude '$page'\n";
318 6         37 eval {
319 6         73 $rester->put_page($page, $new_content);
320             };
321 6 50       64 $self->_handle_error($@, $page, $new_content) if $@;
322             }
323              
324             # Unescape special wafls
325 14         85 for my $w (@special_wafls) {
326 28 100       147 next if $w->[2];
327 14         78 my $wafl = $w->[0];
328 14         64 my $expanded = $w->[1];
329 14         195 $text =~ s/\Q$expanded\E\b/$wafl/ig;
330             }
331              
332 14         142 return $text;
333             }
334              
335             }
336              
337             sub _handle_error {
338 2     2   10 my ($self, $err, $page, $content) = @_;
339 2         35 my $file = Socialtext::Resting::_name_to_id($page) . ".sav";
340 2         83 my $i = 0;
341 2         32 while (-f $file) {
342 1         10 $i++;
343 1         27 $file =~ s/\.sav(?:\.\d+)?$/\.sav\.$i/;
344             }
345 2         282 warn "Failed to write '$page', saving to $file\n";
346 2         14 _write_file($file, $content);
347 2         35 die "$err\n";
348             }
349              
350             sub _write_file {
351 17     17   41 my ($filename, $content) = @_;
352 17   33     117 $filename ||= File::Temp->new( SUFFIX => '.wiki' );
353 17 50       528 open(my $fh, ">$filename") or die "Can't open $filename: $!";
354 17   50     1525 print $fh $content || '';
355 17 50       1674 close $fh or die "Can't write $filename: $!";
356 17         74 return $filename;
357             }
358              
359             sub _read_file {
360 19     19   3009 my $filename = shift;
361 19 50       1277 open(my $fh, $filename) or die "unable to open $filename $!\n";
362 19         2476 my $new_content;
363             {
364 19         40 local $/;
  19         249  
365 19         995 $new_content = <$fh>;
366             }
367 19         255 close $fh;
368 19         506 return $new_content;
369             }
370              
371             =head1 AUTHOR
372              
373             Luke Closs, C<< >>
374              
375             =head1 BUGS
376              
377             Please report any bugs or feature requests to
378             C, or through the web interface at
379             L.
380             I will be notified, and then you'll automatically be notified of progress on
381             your bug as I make changes.
382              
383             =head1 SUPPORT
384              
385             You can find documentation for this module with the perldoc command.
386              
387             perldoc Socialtext::EditPage
388              
389             You can also look for information at:
390              
391             =over 4
392              
393             =item * AnnoCPAN: Annotated CPAN documentation
394              
395             L
396              
397             =item * CPAN Ratings
398              
399             L
400              
401             =item * RT: CPAN's request tracker
402              
403             L
404              
405             =item * Search CPAN
406              
407             L
408              
409             =back
410              
411             =head1 ACKNOWLEDGEMENTS
412              
413             =head1 COPYRIGHT & LICENSE
414              
415             Copyright 2006 Luke Closs, all rights reserved.
416              
417             This program is free software; you can redistribute it and/or modify it
418             under the same terms as Perl itself.
419              
420             =cut
421              
422             1;