blob: 61cd6ac3fb55dec4fd0dda1fde783625b573b3a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
/*
 * The repository summary page and the about page. The summary stacks the
 * branch list, the tag list and the head of the log into one table and ends
 * with the clone urls, reusing the listings ui-refs.c and ui-log.c draw
 * elsewhere. The about page renders the readme that config parsing picked for
 * the repository, either through the configured about filter or escaped as
 * plain text, and it can serve a file sitting beside that readme so links
 * inside the readme resolve.
 */

#include "cgit.h"
#include "filter.h"
#include "html.h"
#include "shared.h"
#include "ui-blob.h"
#include "ui-log.h"
#include "ui-plain.h"
#include "ui-refs.h"
#include "ui-shared.h"
#include "ui-summary.h"

// Age, commit message and author, the three columns a log row always has.
// ui-log.c adds one more for each of the two optional counts, and the rows
// this page stretches across the table have to match that width.
#define LOG_BASE_COLUMNS 3

static int clone_urls_printed;

static int log_columns(void)
{
	int columns = LOG_BASE_COLUMNS;

	if (ctx.repo->enable_log_filecount)
		columns++;
	if (ctx.repo->enable_log_linecount)
		columns++;
	return columns;
}

static void print_clone_url(const char *url)
{
	int columns = log_columns();

	// cgit_add_clone_urls may call back no times at all, so the heading
	// waits for a first url rather than being printed ahead of the walk.
	if (clone_urls_printed++ == 0) {
		html("<tbody class='clone'>\n");
		htmlf("<tr class='nohover'><th colspan='%d' class='left' scope='col'>Clone</th></tr>\n",
			columns);
	}

	cgit_print_clone_row(url, columns);
}

/*
 * Without the separator boundary a sibling directory that merely shares the
 * base as a name prefix, such as repo.git-backup beside repo.git, would pass.
 */
static int path_within(const char *base, const char *path)
{
	size_t len = strlen(base);

	return starts_with(path, base) && (path[len] == '\0' || path[len] == '/');
}

/*
 * Returns a path the caller must free, or NULL when the request cannot be
 * served. A null ref means the readme is a file on the server's disk, and
 * such a readme is confined to its own directory, so one named without a
 * directory is refused.
 */
static char *resolve_about_path(const char *filename, const char *ref, const char *path)
{
	char *copy, *base_dir, *full_path;
	char *resolved_base = NULL, *resolved_full = NULL, *resolved_repo = NULL;

	// dirname is allowed to write into its argument and to return a
	// pointer into it, so base_dir borrows from a copy freed at the end.
	copy = xstrdup(filename);
	base_dir = dirname(copy);
	if (!strcmp(base_dir, ".") || !strcmp(base_dir, "..")) {
		if (!ref) {
			free(copy);
			return NULL;
		}
		full_path = xstrdup(path);
	} else {
		full_path = cgit_fmtalloc("%s/%s", base_dir, path);
	}

	// A readme in the repository directory, or above it, would open the
	// repository's own files, its config and hooks among them.
	if (!ref) {
		resolved_base = realpath(base_dir, NULL);
		resolved_full = realpath(full_path, NULL);
		resolved_repo = realpath(ctx.repo->path, NULL);
		if (
			!resolved_base || !resolved_full || !resolved_repo ||
			path_within(resolved_base, resolved_repo)          ||
			!path_within(resolved_base, resolved_full)
		) {
			free(full_path);
			full_path = NULL;
		}
	}

	free(copy);
	free(resolved_base);
	free(resolved_full);
	free(resolved_repo);

	return full_path;
}

void cgit_print_summary(void)
{
	cgit_print_layout_start();
	// One table, so the columns line up across the sections, each of
	// which is a body of its own.
	html("<table class='list summary' aria-label='Summary'>\n");
	cgit_print_branches(ctx.cfg.summary_branches);
	cgit_print_tags(ctx.cfg.summary_tags);
	if (ctx.cfg.summary_log > 0)
		cgit_print_log(ctx.qry.head, 0, ctx.cfg.summary_log, NULL, NULL, NULL, 0, 0, 0);
	clone_urls_printed = 0;
	cgit_add_clone_urls(print_clone_url);
	if (clone_urls_printed)
		html("</tbody>\n");
	html("</table>\n");
	cgit_print_layout_end();
}

void cgit_print_repo_readme(const char *path)
{
	char *filename, *ref, *mimetype;
	int free_filename = 0;

	mimetype = cgit_get_mimetype_for_filename(path);
	if (mimetype && (starts_with(mimetype, "image/") || starts_with(mimetype, "video/"))) {
		ctx.page.mimetype = mimetype;
		cgit_print_plain();
		free(mimetype);
		return;
	}
	free(mimetype);

	cgit_print_layout_start();
	if (ctx.repo->readme.nr == 0)
		goto done;

	filename = ctx.repo->readme.items[0].string;
	ref = ctx.repo->readme.items[0].util;

	if (path) {
		free_filename = 1;
		filename = resolve_about_path(filename, ref, path);
		if (!filename)
			goto done;
	}

	html("<div id='about'>");
	if (!ctx.repo->about_filter) {
		// With no about-filter there is nothing to turn the readme
		// into safe HTML, so it is escaped rather than served raw.
		html("<pre class='plaintext'>");
		if (ref) {
			cgit_print_file(filename, ref, 1, 1);
		} else {
			struct strbuf sb = STRBUF_INIT;
			if (strbuf_read_file(&sb, filename, 0) >= 0)
				html_txt(sb.buf);
			strbuf_release(&sb);
		}
		html("</pre>");
	} else {
		// The filter is what makes the source safe here, so it is fed
		// through unescaped.
		cgit_open_filter(ctx.repo->about_filter, filename);
		if (ref)
			cgit_print_file(filename, ref, 1, 0);
		else
			html_include(filename);
		cgit_close_filter(ctx.repo->about_filter);
	}

	html("</div>");
	if (free_filename)
		free(filename);

done:
	cgit_print_layout_end();
}