File: | lib/Yukki/Web/Controller/Page.pm |
Coverage: | 32.0% |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Yukki::Web::Controller::Page; | ||||||
2 | |||||||
3 | 1 1 | 1095 5 | use v5.24; | ||||
4 | 1 1 1 | 6 2 7 | use utf8; | ||||
5 | 1 1 1 | 21 3 6 | use Moo; | ||||
6 | |||||||
7 | with 'Yukki::Web::Controller'; | ||||||
8 | |||||||
9 | 1 1 1 | 409 3 62 | use Try::Tiny; | ||||
10 | 1 1 1 | 6 3 7 | use Yukki::Error qw( http_throw ); | ||||
11 | |||||||
12 | 1 1 1 | 355 3 6 | use namespace::clean; | ||||
13 | |||||||
14 | # ABSTRACT: controller for viewing and editing pages | ||||||
15 | |||||||
16 - 26 | =head1 DESCRIPTION Controller for viewing and editing pages =head1 METHODS =head2 fire On a view request routes to L</view_page>, edit request to L</edit_page>, preview request to L</preview_page>, and attach request to L</upload_attachment>. =cut | ||||||
27 | |||||||
28 | sub fire { | ||||||
29 | 1 | 1 | 3 | my ($self, $ctx) = @_; | |||
30 | |||||||
31 | 1 | 13 | my $action = $ctx->request->path_parameters->{action}; | ||||
32 | 1 1 | 21 4 | if ($action eq 'view') { $self->view_page($ctx) } | ||||
33 | 0 | 0 | elsif ($action eq 'edit') { $self->edit_page($ctx) } | ||||
34 | 0 | 0 | elsif ($action eq 'history') { $self->view_history($ctx) } | ||||
35 | 0 | 0 | elsif ($action eq 'diff') { $self->view_diff($ctx) } | ||||
36 | 0 | 0 | elsif ($action eq 'preview') { $self->preview_page($ctx) } | ||||
37 | 0 | 0 | elsif ($action eq 'attach') { $self->upload_attachment($ctx) } | ||||
38 | 0 | 0 | elsif ($action eq 'rename') { $self->rename_page($ctx) } | ||||
39 | 0 | 0 | elsif ($action eq 'remove') { $self->remove_page($ctx) } | ||||
40 | else { | ||||||
41 | 0 | 0 | http_throw('That page action does not exist.', { | ||||
42 | status => 'NotFound', | ||||||
43 | }); | ||||||
44 | } | ||||||
45 | } | ||||||
46 | |||||||
47 - 51 | =head2 repo_name_and_path This is a helper for looking up the repository name and path for the request. =cut | ||||||
52 | |||||||
53 | sub repo_name_and_path { | ||||||
54 | 1 | 1 | 2 | my ($self, $ctx) = @_; | |||
55 | |||||||
56 | 1 | 13 | my $repo_name = $ctx->request->path_parameters->{repository}; | ||||
57 | 1 | 28 | my $path = $ctx->request->path_parameters->{page}; | ||||
58 | |||||||
59 | 1 | 18 | if (not defined $path) { | ||||
60 | my $repo_config | ||||||
61 | 1 | 14 | = $self->app->settings->repositories->{$repo_name}; | ||||
62 | |||||||
63 | 1 | 6 | my $path_str = $repo_config->default_page; | ||||
64 | |||||||
65 | 1 | 5 | $path = [ split m{/}, $path_str ]; | ||||
66 | } | ||||||
67 | |||||||
68 | 1 | 7 | return ($repo_name, $path); | ||||
69 | } | ||||||
70 | |||||||
71 - 75 | =head2 lookup_page Given a repository name and page, returns a L<Yukki::Model::File> for it. =cut | ||||||
76 | |||||||
77 | sub lookup_page { | ||||||
78 | 1 | 1 | 2 | my ($self, $repo_name, $page) = @_; | |||
79 | |||||||
80 | 1 | 15 | my $repository = $self->model('Repository', { name => $repo_name }); | ||||
81 | |||||||
82 | 1 | 1877 | my $final_part = pop @$page; | ||||
83 | 1 | 1 | my $filetype; | ||||
84 | 1 | 9 | if ($final_part =~ s/\.(?<filetype>[a-z0-9]+)$//) { | ||||
85 | 1 | 15 | $filetype = $+{filetype}; | ||||
86 | } | ||||||
87 | |||||||
88 | 1 | 3 | my $path = join '/', @$page, $final_part; | ||||
89 | 1 | 4 | return $repository->file({ path => $path, filetype => $filetype }); | ||||
90 | } | ||||||
91 | |||||||
92 - 97 | =head2 view_page Tells either L<Yukki::Web::View::Page/blank> or L<Yukki::Web::View::Page/view> to show the page. =cut | ||||||
98 | |||||||
99 | sub view_page { | ||||||
100 | 1 | 1 | 2 | my ($self, $ctx) = @_; | |||
101 | |||||||
102 | 1 | 3 | my ($repo_name, $path) = $self->repo_name_and_path($ctx); | ||||
103 | |||||||
104 | 1 | 3 | my $page = $self->lookup_page($repo_name, $path); | ||||
105 | |||||||
106 | 1 | 99 | my $breadcrumb = $self->breadcrumb($page->repository, $path); | ||||
107 | |||||||
108 | 1 | 1 | my $body; | ||||
109 | 1 | 4 | if (not $page->exists) { | ||||
110 | 0 | 0 | my @files = $page->list_files; | ||||
111 | |||||||
112 | 0 | 0 | $body = $self->view('Page')->blank($ctx, { | ||||
113 | title => $page->file_name, | ||||||
114 | breadcrumb => $breadcrumb, | ||||||
115 | repository => $repo_name, | ||||||
116 | page => $page->full_path, | ||||||
117 | files => \@files, | ||||||
118 | }); | ||||||
119 | } | ||||||
120 | |||||||
121 | else { | ||||||
122 | 1 | 119 | $body = $self->view('Page')->view($ctx, { | ||||
123 | title => $page->title, | ||||||
124 | breadcrumb => $breadcrumb, | ||||||
125 | repository => $repo_name, | ||||||
126 | page => $page->full_path, | ||||||
127 | file => $page, | ||||||
128 | }); | ||||||
129 | } | ||||||
130 | |||||||
131 | 1 | 37584 | $ctx->response->body($body); | ||||
132 | } | ||||||
133 | |||||||
134 - 138 | =head2 edit_page Displays or processes the edit form for a page using. =cut | ||||||
139 | |||||||
140 | sub edit_page { | ||||||
141 | 0 | 1 | 0 | my ($self, $ctx) = @_; | |||
142 | |||||||
143 | 0 | 0 | my ($repo_name, $path) = $self->repo_name_and_path($ctx); | ||||
144 | |||||||
145 | 0 | 0 | my $page = $self->lookup_page($repo_name, $path); | ||||
146 | |||||||
147 | 0 | 0 | my $breadcrumb = $self->breadcrumb($page->repository, $path); | ||||
148 | |||||||
149 | 0 | 0 | if ($ctx->request->method eq 'POST') { | ||||
150 | 0 | 0 | my $new_content = $ctx->request->parameters->{yukkitext}; | ||||
151 | 0 | 0 | my $position = $ctx->request->parameters->{yukkitext_position}; | ||||
152 | 0 | 0 | my $comment = $ctx->request->parameters->{comment}; | ||||
153 | |||||||
154 | 0 | 0 | if (my $user = $ctx->session->{user}) { | ||||
155 | 0 | 0 | $page->author_name($user->{name}); | ||||
156 | 0 | 0 | $page->author_email($user->{email}); | ||||
157 | } | ||||||
158 | |||||||
159 | $page->store({ | ||||||
160 | 0 | 0 | content => $new_content, | ||||
161 | comment => $comment, | ||||||
162 | }); | ||||||
163 | |||||||
164 | 0 | 0 | $ctx->response->redirect(join '/', '/page/edit', $repo_name, $page->full_path, '?yukkitext_position='.$position); | ||||
165 | 0 | 0 | return; | ||||
166 | } | ||||||
167 | |||||||
168 | 0 0 | 0 0 | my @attachments = grep { $_->filetype ne 'yukki' } $page->list_files; | ||||
169 | 0 | 0 | my $position = $ctx->request->parameters->{yukkitext_position} // -1; | ||||
170 | |||||||
171 | 0 | 0 | $ctx->response->body( | ||||
172 | $self->view('Page')->edit($ctx, { | ||||||
173 | title => $page->title, | ||||||
174 | breadcrumb => $breadcrumb, | ||||||
175 | repository => $repo_name, | ||||||
176 | page => $page->full_path, | ||||||
177 | position => $position, | ||||||
178 | file => $page, | ||||||
179 | attachments => \@attachments, | ||||||
180 | }) | ||||||
181 | ); | ||||||
182 | } | ||||||
183 | |||||||
184 - 188 | =head2 rename_page Displays the rename page form. =cut | ||||||
189 | |||||||
190 | sub rename_page { | ||||||
191 | 0 | 1 | 0 | my ($self, $ctx) = @_; | |||
192 | |||||||
193 | 0 | 0 | my ($repo_name, $path) = $self->repo_name_and_path($ctx); | ||||
194 | |||||||
195 | 0 | 0 | my $page = $self->lookup_page($repo_name, $path); | ||||
196 | |||||||
197 | 0 | 0 | my $breadcrumb = $self->breadcrumb($page->repository, $path); | ||||
198 | |||||||
199 | 0 | 0 | if ($ctx->request->method eq 'POST') { | ||||
200 | 0 | 0 | my $new_name = $ctx->request->parameters->{yukkiname_new}; | ||||
201 | |||||||
202 | 0 | 0 | my $part = qr{[_a-z0-9-.]+(?:\.[_a-z0-9-]+)*}i; | ||||
203 | 0 | 0 | if ($new_name =~ m{^$part(?:/$part)*$}) { | ||||
204 | |||||||
205 | 0 | 0 | if (my $user = $ctx->session->{user}) { | ||||
206 | 0 | 0 | $page->author_name($user->{name}); | ||||
207 | 0 | 0 | $page->author_email($user->{email}); | ||||
208 | } | ||||||
209 | |||||||
210 | $page->rename({ | ||||||
211 | 0 | 0 | full_path => $new_name, | ||||
212 | comment => 'Renamed ' . $page->full_path . ' to ' . $new_name, | ||||||
213 | }); | ||||||
214 | |||||||
215 | 0 | 0 | $ctx->response->redirect(join '/', '/page/edit', $repo_name, $new_name); | ||||
216 | 0 | 0 | return; | ||||
217 | |||||||
218 | } | ||||||
219 | else { | ||||||
220 | 0 | 0 | $ctx->add_errors('the new name must contain only letters, numbers, underscores, dashes, periods, and slashes'); | ||||
221 | } | ||||||
222 | } | ||||||
223 | |||||||
224 | $ctx->response->body( | ||||||
225 | 0 | 0 | $self->view('Page')->rename($ctx, { | ||||
226 | title => $page->title, | ||||||
227 | breadcrumb => $breadcrumb, | ||||||
228 | repository => $repo_name, | ||||||
229 | page => $page->full_path, | ||||||
230 | file => $page, | ||||||
231 | }) | ||||||
232 | ); | ||||||
233 | } | ||||||
234 | |||||||
235 - 239 | =head2 remove_page Displays the remove confirmation. =cut | ||||||
240 | |||||||
241 | sub remove_page { | ||||||
242 | 0 | 1 | 0 | my ($self, $ctx) = @_; | |||
243 | |||||||
244 | 0 | 0 | my ($repo_name, $path) = $self->repo_name_and_path($ctx); | ||||
245 | |||||||
246 | 0 | 0 | my $page = $self->lookup_page($repo_name, $path); | ||||
247 | |||||||
248 | 0 | 0 | my $breadcrumb = $self->breadcrumb($page->repository, $path); | ||||
249 | |||||||
250 | 0 | 0 | my $confirmed = $ctx->request->body_parameters->{confirmed}; | ||||
251 | 0 | 0 | if ($ctx->request->method eq 'POST' and $confirmed) { | ||||
252 | 0 | 0 | my $return_to = $page->parent // $page->repository->default_file; | ||||
253 | 0 | 0 | if ($return_to->full_path ne $page->full_path) { | ||||
254 | 0 | 0 | if (my $user = $ctx->session->{user}) { | ||||
255 | 0 | 0 | $page->author_name($user->{name}); | ||||
256 | 0 | 0 | $page->author_email($user->{email}); | ||||
257 | } | ||||||
258 | |||||||
259 | $page->remove({ | ||||||
260 | 0 | 0 | comment => 'Removing ' . $page->full_path . ' from repository.', | ||||
261 | }); | ||||||
262 | |||||||
263 | 0 | 0 | $ctx->response->redirect(join '/', '/page/view', $repo_name, $return_to->full_path); | ||||
264 | 0 | 0 | return; | ||||
265 | |||||||
266 | } | ||||||
267 | |||||||
268 | else { | ||||||
269 | 0 | 0 | $ctx->add_errors('you may not remove the top-most page of a repository'); | ||||
270 | } | ||||||
271 | } | ||||||
272 | |||||||
273 | $ctx->response->body( | ||||||
274 | 0 | 0 | $self->view('Page')->remove($ctx, { | ||||
275 | title => $page->title, | ||||||
276 | breadcrumb => $breadcrumb, | ||||||
277 | repository => $repo_name, | ||||||
278 | page => $page->full_path, | ||||||
279 | file => $page, | ||||||
280 | return_link => join('/', '/page/view', $repo_name, $page->full_path), | ||||||
281 | }) | ||||||
282 | ); | ||||||
283 | } | ||||||
284 | |||||||
285 - 289 | =head2 view_history Displays the page's revision history. =cut | ||||||
290 | |||||||
291 | sub view_history { | ||||||
292 | 0 | 1 | 0 | my ($self, $ctx) = @_; | |||
293 | |||||||
294 | 0 | 0 | my ($repo_name, $path) = $self->repo_name_and_path($ctx); | ||||
295 | |||||||
296 | 0 | 0 | my $page = $self->lookup_page($repo_name, $path); | ||||
297 | |||||||
298 | 0 | 0 | my $breadcrumb = $self->breadcrumb($page->repository, $path); | ||||
299 | |||||||
300 | 0 | 0 | $ctx->response->body( | ||||
301 | $self->view('Page')->history($ctx, { | ||||||
302 | title => $page->title, | ||||||
303 | breadcrumb => $breadcrumb, | ||||||
304 | repository => $repo_name, | ||||||
305 | page => $page->full_path, | ||||||
306 | revisions => [ $page->history ], | ||||||
307 | }) | ||||||
308 | ); | ||||||
309 | } | ||||||
310 | |||||||
311 - 315 | =head2 view_diff Displays a diff of the page. =cut | ||||||
316 | |||||||
317 | sub view_diff { | ||||||
318 | 0 | 1 | 0 | my ($self, $ctx) = @_; | |||
319 | |||||||
320 | 0 | 0 | my ($repo_name, $path) = $self->repo_name_and_path($ctx); | ||||
321 | |||||||
322 | 0 | 0 | my $page = $self->lookup_page($repo_name, $path); | ||||
323 | |||||||
324 | 0 | 0 | my $breadcrumb = $self->breadcrumb($page->repository, $path); | ||||
325 | |||||||
326 | 0 | 0 | my $r1 = $ctx->request->query_parameters->{r1}; | ||||
327 | 0 | 0 | my $r2 = $ctx->request->query_parameters->{r2}; | ||||
328 | |||||||
329 | try { | ||||||
330 | |||||||
331 | 0 | 0 | my $diff = ''; | ||||
332 | 0 | 0 | for my $chunk ($page->diff($r1, $r2)) { | ||||
333 | 0 0 | 0 0 | if ($chunk->[0] eq ' ') { $diff .= $chunk->[1] } | ||||
334 | 0 | 0 | elsif ($chunk->[0] eq '+') { $diff .= sprintf '<ins markdown="1">%s</ins>', $chunk->[1] } | ||||
335 | 0 | 0 | elsif ($chunk->[0] eq '-') { $diff .= sprintf '<del markdown="1">%s</del>', $chunk->[1] } | ||||
336 | 0 | 0 | else { warn "unknown chunk type $chunk->[0]" } | ||||
337 | } | ||||||
338 | |||||||
339 | 0 | 0 | my $file_preview = $page->file_preview( | ||||
340 | content => $diff, | ||||||
341 | ); | ||||||
342 | |||||||
343 | 0 | 0 | $ctx->response->body( | ||||
344 | $self->view('Page')->diff($ctx, { | ||||||
345 | title => $page->title, | ||||||
346 | breadcrumb => $breadcrumb, | ||||||
347 | repository => $repo_name, | ||||||
348 | page => $page->full_path, | ||||||
349 | file => $file_preview, | ||||||
350 | }) | ||||||
351 | ); | ||||||
352 | } | ||||||
353 | |||||||
354 | catch { | ||||||
355 | 0 | 0 | my $ERROR = $_; | ||||
356 | 0 | 0 | if ("$_" =~ /usage: git diff/) { | ||||
357 | 0 | 0 | http_throw 'Diffs will not work with git versions before 1.7.2. Please use a newer version of git. If you are using a newer version of git, please file a support issue.'; | ||||
358 | } | ||||||
359 | 0 | 0 | die $ERROR; | ||||
360 | 0 | 0 | }; | ||||
361 | } | ||||||
362 | |||||||
363 - 367 | =head2 preview_page Shows the preview for an edit to a page using L<Yukki::Web::View::Page/preview>.. =cut | ||||||
368 | |||||||
369 | sub preview_page { | ||||||
370 | 0 | 1 | 0 | my ($self, $ctx) = @_; | |||
371 | |||||||
372 | 0 | 0 | my ($repo_name, $path) = $self->repo_name_and_path($ctx); | ||||
373 | |||||||
374 | 0 | 0 | my $page = $self->lookup_page($repo_name, $path); | ||||
375 | |||||||
376 | 0 | 0 | my $breadcrumb = $self->breadcrumb($page->repository, $path); | ||||
377 | |||||||
378 | 0 | 0 | my $content = $ctx->request->body_parameters->{yukkitext}; | ||||
379 | 0 | 0 | my $position = $ctx->request->parameters->{yukkitext_position}; | ||||
380 | 0 | 0 | my $file_preview = $page->file_preview( | ||||
381 | content => $content, | ||||||
382 | position => $position, | ||||||
383 | ); | ||||||
384 | |||||||
385 | 0 | 0 | $ctx->response->body( | ||||
386 | $self->view('Page')->preview($ctx, { | ||||||
387 | title => $page->title, | ||||||
388 | breadcrumb => $breadcrumb, | ||||||
389 | repository => $repo_name, | ||||||
390 | page => $page->full_path, | ||||||
391 | file => $file_preview, | ||||||
392 | }) | ||||||
393 | ); | ||||||
394 | } | ||||||
395 | |||||||
396 - 400 | =head2 upload_attachment This is a facade that wraps L<Yukki::Web::Controller::Attachment/upload>. =cut | ||||||
401 | |||||||
402 | sub upload_attachment { | ||||||
403 | 0 | 1 | 0 | my ($self, $ctx) = @_; | |||
404 | |||||||
405 | 0 | 0 | my $repo_name = $ctx->request->path_parameters->{repository}; | ||||
406 | 0 | 0 | my $path = delete $ctx->request->path_parameters->{page}; | ||||
407 | |||||||
408 | 0 | 0 | my $page = $self->lookup_page($repo_name, $path); | ||||
409 | |||||||
410 | 0 | 0 | my @file = split m{/}, $page->path; | ||||
411 | 0 | 0 | push @file, $ctx->request->uploads->{file}->filename; | ||||
412 | |||||||
413 | 0 | 0 | $ctx->request->path_parameters->{action} = 'upload'; | ||||
414 | 0 | 0 | $ctx->request->path_parameters->{file} = \@file; | ||||
415 | |||||||
416 | 0 | 0 | $self->controller('Attachment')->fire($ctx); | ||||
417 | } | ||||||
418 | |||||||
419 - 423 | =head2 breadcrumb Given the repository and path, returns the breadcrumb. =cut | ||||||
424 | |||||||
425 | sub breadcrumb { | ||||||
426 | 1 | 1 | 2 | my ($self, $repository, $path_parts) = @_; | |||
427 | |||||||
428 | 1 | 2 | my @breadcrumb; | ||||
429 | my @path_acc; | ||||||
430 | |||||||
431 | 1 | 12 | push @breadcrumb, { | ||||
432 | label => $repository->title, | ||||||
433 | href => join('/', '/page/view/', $repository->name), | ||||||
434 | }; | ||||||
435 | |||||||
436 | 1 | 38 | for my $path_part (@$path_parts) { | ||||
437 | 0 | 0 | push @path_acc, $path_part; | ||||
438 | 0 | 0 | my $file = $repository->file({ | ||||
439 | path => join('/', @path_acc), | ||||||
440 | filetype => 'yukki', | ||||||
441 | }); | ||||||
442 | |||||||
443 | 0 | 0 | push @breadcrumb, { | ||||
444 | label => $file->title, | ||||||
445 | href => join('/', '/page/view', $repository->name, $file->full_path), | ||||||
446 | }; | ||||||
447 | } | ||||||
448 | |||||||
449 | 1 | 2 | return \@breadcrumb; | ||||
450 | } | ||||||
451 | |||||||
452 | 1; |