File Coverage

File:lib/Yukki/Web/Controller/Page.pm
Coverage:32.0%

linestmtbrancondsubpodtimecode
1package 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
7with '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
28sub 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
53sub 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
77sub 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
99sub 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
140sub 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
190sub 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
241sub 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
291sub 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
317sub 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
369sub 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
402sub 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
425sub 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
4521;