]> gitweb.ps.run Git - ps-cgit/blob - ui-shared.c
git: update to v2.46.0
[ps-cgit] / ui-shared.c
1 /* ui-shared.c: common web output functions
2  *
3  * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
4  *
5  * Licensed under GNU General Public License v2
6  *   (see COPYING for full license text)
7  */
8
9 #define USE_THE_REPOSITORY_VARIABLE
10
11 #include "cgit.h"
12 #include "ui-shared.h"
13 #include "cmd.h"
14 #include "html.h"
15 #include "version.h"
16
17 static const char cgit_doctype[] =
18 "<!DOCTYPE html>\n";
19
20 static char *http_date(time_t t)
21 {
22         static char day[][4] =
23                 {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
24         static char month[][4] =
25                 {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
26                  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
27         struct tm tm;
28         gmtime_r(&t, &tm);
29         return fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm.tm_wday],
30                    tm.tm_mday, month[tm.tm_mon], 1900 + tm.tm_year,
31                    tm.tm_hour, tm.tm_min, tm.tm_sec);
32 }
33
34 void cgit_print_error(const char *fmt, ...)
35 {
36         va_list ap;
37         va_start(ap, fmt);
38         cgit_vprint_error(fmt, ap);
39         va_end(ap);
40 }
41
42 void cgit_vprint_error(const char *fmt, va_list ap)
43 {
44         va_list cp;
45         html("<div class='error'>");
46         va_copy(cp, ap);
47         html_vtxtf(fmt, cp);
48         va_end(cp);
49         html("</div>\n");
50 }
51
52 const char *cgit_httpscheme(void)
53 {
54         if (ctx.env.https && !strcmp(ctx.env.https, "on"))
55                 return "https://";
56         else
57                 return "http://";
58 }
59
60 char *cgit_hosturl(void)
61 {
62         if (ctx.env.http_host)
63                 return xstrdup(ctx.env.http_host);
64         if (!ctx.env.server_name)
65                 return NULL;
66         if (!ctx.env.server_port || atoi(ctx.env.server_port) == 80)
67                 return xstrdup(ctx.env.server_name);
68         return fmtalloc("%s:%s", ctx.env.server_name, ctx.env.server_port);
69 }
70
71 char *cgit_currenturl(void)
72 {
73         const char *root = cgit_rooturl();
74
75         if (!ctx.qry.url)
76                 return xstrdup(root);
77         if (root[0] && root[strlen(root) - 1] == '/')
78                 return fmtalloc("%s%s", root, ctx.qry.url);
79         return fmtalloc("%s/%s", root, ctx.qry.url);
80 }
81
82 char *cgit_currentfullurl(void)
83 {
84         const char *root = cgit_rooturl();
85         const char *orig_query = ctx.env.query_string ? ctx.env.query_string : "";
86         size_t len = strlen(orig_query);
87         char *query = xmalloc(len + 2), *start_url, *ret;
88
89         /* Remove all url=... parts from query string */
90         memcpy(query + 1, orig_query, len + 1);
91         query[0] = '?';
92         start_url = query;
93         while ((start_url = strstr(start_url, "url=")) != NULL) {
94                 if (start_url[-1] == '?' || start_url[-1] == '&') {
95                         const char *end_url = strchr(start_url, '&');
96                         if (end_url)
97                                 memmove(start_url, end_url + 1, strlen(end_url));
98                         else
99                                 start_url[0] = '\0';
100                 } else
101                         ++start_url;
102         }
103         if (!query[1])
104                 query[0] = '\0';
105
106         if (!ctx.qry.url)
107                 ret = fmtalloc("%s%s", root, query);
108         else if (root[0] && root[strlen(root) - 1] == '/')
109                 ret = fmtalloc("%s%s%s", root, ctx.qry.url, query);
110         else
111                 ret = fmtalloc("%s/%s%s", root, ctx.qry.url, query);
112         free(query);
113         return ret;
114 }
115
116 const char *cgit_rooturl(void)
117 {
118         if (ctx.cfg.virtual_root)
119                 return ctx.cfg.virtual_root;
120         else
121                 return ctx.cfg.script_name;
122 }
123
124 const char *cgit_loginurl(void)
125 {
126         static const char *login_url;
127         if (!login_url)
128                 login_url = fmtalloc("%s?p=login", cgit_rooturl());
129         return login_url;
130 }
131
132 char *cgit_repourl(const char *reponame)
133 {
134         if (ctx.cfg.virtual_root)
135                 return fmtalloc("%s%s/", ctx.cfg.virtual_root, reponame);
136         else
137                 return fmtalloc("?r=%s", reponame);
138 }
139
140 char *cgit_fileurl(const char *reponame, const char *pagename,
141                    const char *filename, const char *query)
142 {
143         struct strbuf sb = STRBUF_INIT;
144         char *delim;
145
146         if (ctx.cfg.virtual_root) {
147                 strbuf_addf(&sb, "%s%s/%s/%s", ctx.cfg.virtual_root, reponame,
148                             pagename, (filename ? filename:""));
149                 delim = "?";
150         } else {
151                 strbuf_addf(&sb, "?url=%s/%s/%s", reponame, pagename,
152                             (filename ? filename : ""));
153                 delim = "&amp;";
154         }
155         if (query)
156                 strbuf_addf(&sb, "%s%s", delim, query);
157         return strbuf_detach(&sb, NULL);
158 }
159
160 char *cgit_pageurl(const char *reponame, const char *pagename,
161                    const char *query)
162 {
163         return cgit_fileurl(reponame, pagename, NULL, query);
164 }
165
166 const char *cgit_repobasename(const char *reponame)
167 {
168         /* I assume we don't need to store more than one repo basename */
169         static char rvbuf[1024];
170         int p;
171         const char *rv;
172         size_t len;
173
174         len = strlcpy(rvbuf, reponame, sizeof(rvbuf));
175         if (len >= sizeof(rvbuf))
176                 die("cgit_repobasename: truncated repository name '%s'", reponame);
177         p = len - 1;
178         /* strip trailing slashes */
179         while (p && rvbuf[p] == '/')
180                 rvbuf[p--] = '\0';
181         /* strip trailing .git */
182         if (p >= 3 && starts_with(&rvbuf[p-3], ".git")) {
183                 p -= 3;
184                 rvbuf[p--] = '\0';
185         }
186         /* strip more trailing slashes if any */
187         while (p && rvbuf[p] == '/')
188                 rvbuf[p--] = '\0';
189         /* find last slash in the remaining string */
190         rv = strrchr(rvbuf, '/');
191         if (rv)
192                 return ++rv;
193         return rvbuf;
194 }
195
196 const char *cgit_snapshot_prefix(const struct cgit_repo *repo)
197 {
198         if (repo->snapshot_prefix)
199                 return repo->snapshot_prefix;
200
201         return cgit_repobasename(repo->url);
202 }
203
204 static void site_url(const char *page, const char *search, const char *sort, int ofs, int always_root)
205 {
206         char *delim = "?";
207
208         if (always_root || page)
209                 html_attr(cgit_rooturl());
210         else {
211                 char *currenturl = cgit_currenturl();
212                 html_attr(currenturl);
213                 free(currenturl);
214         }
215
216         if (page) {
217                 htmlf("?p=%s", page);
218                 delim = "&amp;";
219         }
220         if (search) {
221                 html(delim);
222                 html("q=");
223                 html_attr(search);
224                 delim = "&amp;";
225         }
226         if (sort) {
227                 html(delim);
228                 html("s=");
229                 html_attr(sort);
230                 delim = "&amp;";
231         }
232         if (ofs) {
233                 html(delim);
234                 htmlf("ofs=%d", ofs);
235         }
236 }
237
238 static void site_link(const char *page, const char *name, const char *title,
239                       const char *class, const char *search, const char *sort, int ofs, int always_root)
240 {
241         html("<a");
242         if (title) {
243                 html(" title='");
244                 html_attr(title);
245                 html("'");
246         }
247         if (class) {
248                 html(" class='");
249                 html_attr(class);
250                 html("'");
251         }
252         html(" href='");
253         site_url(page, search, sort, ofs, always_root);
254         html("'>");
255         html_txt(name);
256         html("</a>");
257 }
258
259 void cgit_index_link(const char *name, const char *title, const char *class,
260                      const char *pattern, const char *sort, int ofs, int always_root)
261 {
262         site_link(NULL, name, title, class, pattern, sort, ofs, always_root);
263 }
264
265 static char *repolink(const char *title, const char *class, const char *page,
266                       const char *head, const char *path)
267 {
268         char *delim = "?";
269
270         html("<a");
271         if (title) {
272                 html(" title='");
273                 html_attr(title);
274                 html("'");
275         }
276         if (class) {
277                 html(" class='");
278                 html_attr(class);
279                 html("'");
280         }
281         html(" href='");
282         if (ctx.cfg.virtual_root) {
283                 html_url_path(ctx.cfg.virtual_root);
284                 html_url_path(ctx.repo->url);
285                 if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
286                         html("/");
287                 if (page) {
288                         html_url_path(page);
289                         html("/");
290                         if (path)
291                                 html_url_path(path);
292                 }
293         } else {
294                 html_url_path(ctx.cfg.script_name);
295                 html("?url=");
296                 html_url_arg(ctx.repo->url);
297                 if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
298                         html("/");
299                 if (page) {
300                         html_url_arg(page);
301                         html("/");
302                         if (path)
303                                 html_url_arg(path);
304                 }
305                 delim = "&amp;";
306         }
307         if (head && ctx.repo->defbranch && strcmp(head, ctx.repo->defbranch)) {
308                 html(delim);
309                 html("h=");
310                 html_url_arg(head);
311                 delim = "&amp;";
312         }
313         return fmt("%s", delim);
314 }
315
316 static void reporevlink(const char *page, const char *name, const char *title,
317                         const char *class, const char *head, const char *rev,
318                         const char *path)
319 {
320         char *delim;
321
322         delim = repolink(title, class, page, head, path);
323         if (rev && ctx.qry.head != NULL && strcmp(rev, ctx.qry.head)) {
324                 html(delim);
325                 html("id=");
326                 html_url_arg(rev);
327         }
328         html("'>");
329         html_txt(name);
330         html("</a>");
331 }
332
333 void cgit_summary_link(const char *name, const char *title, const char *class,
334                        const char *head)
335 {
336         reporevlink(NULL, name, title, class, head, NULL, NULL);
337 }
338
339 void cgit_tag_link(const char *name, const char *title, const char *class,
340                    const char *tag)
341 {
342         reporevlink("tag", name, title, class, tag, NULL, NULL);
343 }
344
345 void cgit_tree_link(const char *name, const char *title, const char *class,
346                     const char *head, const char *rev, const char *path)
347 {
348         reporevlink("tree", name, title, class, head, rev, path);
349 }
350
351 void cgit_plain_link(const char *name, const char *title, const char *class,
352                      const char *head, const char *rev, const char *path)
353 {
354         reporevlink("plain", name, title, class, head, rev, path);
355 }
356
357 void cgit_blame_link(const char *name, const char *title, const char *class,
358                      const char *head, const char *rev, const char *path)
359 {
360         reporevlink("blame", name, title, class, head, rev, path);
361 }
362
363 void cgit_log_link(const char *name, const char *title, const char *class,
364                    const char *head, const char *rev, const char *path,
365                    int ofs, const char *grep, const char *pattern, int showmsg,
366                    int follow)
367 {
368         char *delim;
369
370         delim = repolink(title, class, "log", head, path);
371         if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) {
372                 html(delim);
373                 html("id=");
374                 html_url_arg(rev);
375                 delim = "&amp;";
376         }
377         if (grep && pattern) {
378                 html(delim);
379                 html("qt=");
380                 html_url_arg(grep);
381                 delim = "&amp;";
382                 html(delim);
383                 html("q=");
384                 html_url_arg(pattern);
385         }
386         if (ofs > 0) {
387                 html(delim);
388                 html("ofs=");
389                 htmlf("%d", ofs);
390                 delim = "&amp;";
391         }
392         if (showmsg) {
393                 html(delim);
394                 html("showmsg=1");
395                 delim = "&amp;";
396         }
397         if (follow) {
398                 html(delim);
399                 html("follow=1");
400         }
401         html("'>");
402         html_txt(name);
403         html("</a>");
404 }
405
406 void cgit_commit_link(const char *name, const char *title, const char *class,
407                       const char *head, const char *rev, const char *path)
408 {
409         char *delim;
410
411         delim = repolink(title, class, "commit", head, path);
412         if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) {
413                 html(delim);
414                 html("id=");
415                 html_url_arg(rev);
416                 delim = "&amp;";
417         }
418         if (ctx.qry.difftype) {
419                 html(delim);
420                 htmlf("dt=%d", ctx.qry.difftype);
421                 delim = "&amp;";
422         }
423         if (ctx.qry.context > 0 && ctx.qry.context != 3) {
424                 html(delim);
425                 html("context=");
426                 htmlf("%d", ctx.qry.context);
427                 delim = "&amp;";
428         }
429         if (ctx.qry.ignorews) {
430                 html(delim);
431                 html("ignorews=1");
432                 delim = "&amp;";
433         }
434         if (ctx.qry.follow) {
435                 html(delim);
436                 html("follow=1");
437         }
438         html("'>");
439         if (name[0] != '\0') {
440                 if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) {
441                         html_ntxt(name, ctx.cfg.max_msg_len - 3);
442                         html("...");
443                 } else
444                         html_txt(name);
445         } else
446                 html_txt("(no commit message)");
447         html("</a>");
448 }
449
450 void cgit_refs_link(const char *name, const char *title, const char *class,
451                     const char *head, const char *rev, const char *path)
452 {
453         reporevlink("refs", name, title, class, head, rev, path);
454 }
455
456 void cgit_snapshot_link(const char *name, const char *title, const char *class,
457                         const char *head, const char *rev,
458                         const char *archivename)
459 {
460         reporevlink("snapshot", name, title, class, head, rev, archivename);
461 }
462
463 void cgit_diff_link(const char *name, const char *title, const char *class,
464                     const char *head, const char *new_rev, const char *old_rev,
465                     const char *path)
466 {
467         char *delim;
468
469         delim = repolink(title, class, "diff", head, path);
470         if (new_rev && ctx.qry.head != NULL && strcmp(new_rev, ctx.qry.head)) {
471                 html(delim);
472                 html("id=");
473                 html_url_arg(new_rev);
474                 delim = "&amp;";
475         }
476         if (old_rev) {
477                 html(delim);
478                 html("id2=");
479                 html_url_arg(old_rev);
480                 delim = "&amp;";
481         }
482         if (ctx.qry.difftype) {
483                 html(delim);
484                 htmlf("dt=%d", ctx.qry.difftype);
485                 delim = "&amp;";
486         }
487         if (ctx.qry.context > 0 && ctx.qry.context != 3) {
488                 html(delim);
489                 html("context=");
490                 htmlf("%d", ctx.qry.context);
491                 delim = "&amp;";
492         }
493         if (ctx.qry.ignorews) {
494                 html(delim);
495                 html("ignorews=1");
496                 delim = "&amp;";
497         }
498         if (ctx.qry.follow) {
499                 html(delim);
500                 html("follow=1");
501         }
502         html("'>");
503         html_txt(name);
504         html("</a>");
505 }
506
507 void cgit_patch_link(const char *name, const char *title, const char *class,
508                      const char *head, const char *rev, const char *path)
509 {
510         reporevlink("patch", name, title, class, head, rev, path);
511 }
512
513 void cgit_stats_link(const char *name, const char *title, const char *class,
514                      const char *head, const char *path)
515 {
516         reporevlink("stats", name, title, class, head, NULL, path);
517 }
518
519 static void cgit_self_link(char *name, const char *title, const char *class)
520 {
521         if (!strcmp(ctx.qry.page, "repolist"))
522                 cgit_index_link(name, title, class, ctx.qry.search, ctx.qry.sort,
523                                 ctx.qry.ofs, 1);
524         else if (!strcmp(ctx.qry.page, "summary"))
525                 cgit_summary_link(name, title, class, ctx.qry.head);
526         else if (!strcmp(ctx.qry.page, "tag"))
527                 cgit_tag_link(name, title, class, ctx.qry.has_oid ?
528                                ctx.qry.oid : ctx.qry.head);
529         else if (!strcmp(ctx.qry.page, "tree"))
530                 cgit_tree_link(name, title, class, ctx.qry.head,
531                                ctx.qry.has_oid ? ctx.qry.oid : NULL,
532                                ctx.qry.path);
533         else if (!strcmp(ctx.qry.page, "plain"))
534                 cgit_plain_link(name, title, class, ctx.qry.head,
535                                 ctx.qry.has_oid ? ctx.qry.oid : NULL,
536                                 ctx.qry.path);
537         else if (!strcmp(ctx.qry.page, "blame"))
538                 cgit_blame_link(name, title, class, ctx.qry.head,
539                                 ctx.qry.has_oid ? ctx.qry.oid : NULL,
540                                 ctx.qry.path);
541         else if (!strcmp(ctx.qry.page, "log"))
542                 cgit_log_link(name, title, class, ctx.qry.head,
543                               ctx.qry.has_oid ? ctx.qry.oid : NULL,
544                               ctx.qry.path, ctx.qry.ofs,
545                               ctx.qry.grep, ctx.qry.search,
546                               ctx.qry.showmsg, ctx.qry.follow);
547         else if (!strcmp(ctx.qry.page, "commit"))
548                 cgit_commit_link(name, title, class, ctx.qry.head,
549                                  ctx.qry.has_oid ? ctx.qry.oid : NULL,
550                                  ctx.qry.path);
551         else if (!strcmp(ctx.qry.page, "patch"))
552                 cgit_patch_link(name, title, class, ctx.qry.head,
553                                 ctx.qry.has_oid ? ctx.qry.oid : NULL,
554                                 ctx.qry.path);
555         else if (!strcmp(ctx.qry.page, "refs"))
556                 cgit_refs_link(name, title, class, ctx.qry.head,
557                                ctx.qry.has_oid ? ctx.qry.oid : NULL,
558                                ctx.qry.path);
559         else if (!strcmp(ctx.qry.page, "snapshot"))
560                 cgit_snapshot_link(name, title, class, ctx.qry.head,
561                                    ctx.qry.has_oid ? ctx.qry.oid : NULL,
562                                    ctx.qry.path);
563         else if (!strcmp(ctx.qry.page, "diff"))
564                 cgit_diff_link(name, title, class, ctx.qry.head,
565                                ctx.qry.oid, ctx.qry.oid2,
566                                ctx.qry.path);
567         else if (!strcmp(ctx.qry.page, "stats"))
568                 cgit_stats_link(name, title, class, ctx.qry.head,
569                                 ctx.qry.path);
570         else {
571                 /* Don't known how to make link for this page */
572                 repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path);
573                 html("><!-- cgit_self_link() doesn't know how to make link for page '");
574                 html_txt(ctx.qry.page);
575                 html("' -->");
576                 html_txt(name);
577                 html("</a>");
578         }
579 }
580
581 void cgit_object_link(struct object *obj)
582 {
583         char *page, *shortrev, *fullrev, *name;
584
585         fullrev = oid_to_hex(&obj->oid);
586         shortrev = xstrdup(fullrev);
587         shortrev[10] = '\0';
588         if (obj->type == OBJ_COMMIT) {
589                 cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL,
590                                  ctx.qry.head, fullrev, NULL);
591                 return;
592         } else if (obj->type == OBJ_TREE)
593                 page = "tree";
594         else if (obj->type == OBJ_TAG)
595                 page = "tag";
596         else
597                 page = "blob";
598         name = fmt("%s %s...", type_name(obj->type), shortrev);
599         reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL);
600 }
601
602 static struct string_list_item *lookup_path(struct string_list *list,
603                                             const char *path)
604 {
605         struct string_list_item *item;
606
607         while (path && path[0]) {
608                 if ((item = string_list_lookup(list, path)))
609                         return item;
610                 if (!(path = strchr(path, '/')))
611                         break;
612                 path++;
613         }
614         return NULL;
615 }
616
617 void cgit_submodule_link(const char *class, char *path, const char *rev)
618 {
619         struct string_list *list;
620         struct string_list_item *item;
621         char tail, *dir;
622         size_t len;
623
624         len = 0;
625         tail = 0;
626         list = &ctx.repo->submodules;
627         item = lookup_path(list, path);
628         if (!item) {
629                 len = strlen(path);
630                 tail = path[len - 1];
631                 if (tail == '/') {
632                         path[len - 1] = 0;
633                         item = lookup_path(list, path);
634                 }
635         }
636         if (item || ctx.repo->module_link) {
637                 html("<a ");
638                 if (class)
639                         htmlf("class='%s' ", class);
640                 html("href='");
641                 if (item) {
642                         html_attrf(item->util, rev);
643                 } else {
644                         dir = strrchr(path, '/');
645                         if (dir)
646                                 dir++;
647                         else
648                                 dir = path;
649                         html_attrf(ctx.repo->module_link, dir, rev);
650                 }
651                 html("'>");
652                 html_txt(path);
653                 html("</a>");
654         } else {
655                 html("<span");
656                 if (class)
657                         htmlf(" class='%s'", class);
658                 html(">");
659                 html_txt(path);
660                 html("</span>");
661         }
662         html_txtf(" @ %.7s", rev);
663         if (item && tail)
664                 path[len - 1] = tail;
665 }
666
667 const struct date_mode cgit_date_mode(enum date_mode_type type)
668 {
669         static struct date_mode mode;
670         mode.type = type;
671         mode.local = ctx.cfg.local_time;
672         return mode;
673 }
674
675 static void print_rel_date(time_t t, int tz, double value,
676         const char *class, const char *suffix)
677 {
678         htmlf("<span class='%s' data-ut='%" PRIu64 "' title='", class, (uint64_t)t);
679         html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601)));
680         htmlf("'>%.0f %s</span>", value, suffix);
681 }
682
683 void cgit_print_age(time_t t, int tz, time_t max_relative)
684 {
685         time_t now, secs;
686
687         if (!t)
688                 return;
689         time(&now);
690         secs = now - t;
691         if (secs < 0)
692                 secs = 0;
693
694         if (secs > max_relative && max_relative >= 0) {
695                 html("<span title='");
696                 html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601)));
697                 html("'>");
698                 html_txt(show_date(t, tz, cgit_date_mode(DATE_SHORT)));
699                 html("</span>");
700                 return;
701         }
702
703         if (secs < TM_HOUR * 2) {
704                 print_rel_date(t, tz, secs * 1.0 / TM_MIN, "age-mins", "min.");
705                 return;
706         }
707         if (secs < TM_DAY * 2) {
708                 print_rel_date(t, tz, secs * 1.0 / TM_HOUR, "age-hours", "hours");
709                 return;
710         }
711         if (secs < TM_WEEK * 2) {
712                 print_rel_date(t, tz, secs * 1.0 / TM_DAY, "age-days", "days");
713                 return;
714         }
715         if (secs < TM_MONTH * 2) {
716                 print_rel_date(t, tz, secs * 1.0 / TM_WEEK, "age-weeks", "weeks");
717                 return;
718         }
719         if (secs < TM_YEAR * 2) {
720                 print_rel_date(t, tz, secs * 1.0 / TM_MONTH, "age-months", "months");
721                 return;
722         }
723         print_rel_date(t, tz, secs * 1.0 / TM_YEAR, "age-years", "years");
724 }
725
726 void cgit_print_http_headers(void)
727 {
728         if (ctx.env.no_http && !strcmp(ctx.env.no_http, "1"))
729                 return;
730
731         if (ctx.page.status)
732                 htmlf("Status: %d %s\n", ctx.page.status, ctx.page.statusmsg);
733         if (ctx.page.mimetype && ctx.page.charset)
734                 htmlf("Content-Type: %s; charset=%s\n", ctx.page.mimetype,
735                       ctx.page.charset);
736         else if (ctx.page.mimetype)
737                 htmlf("Content-Type: %s\n", ctx.page.mimetype);
738         if (ctx.page.size)
739                 htmlf("Content-Length: %zd\n", ctx.page.size);
740         if (ctx.page.filename) {
741                 html("Content-Disposition: inline; filename=\"");
742                 html_header_arg_in_quotes(ctx.page.filename);
743                 html("\"\n");
744         }
745         if (!ctx.env.authenticated)
746                 html("Cache-Control: no-cache, no-store\n");
747         htmlf("Last-Modified: %s\n", http_date(ctx.page.modified));
748         htmlf("Expires: %s\n", http_date(ctx.page.expires));
749         if (ctx.page.etag)
750                 htmlf("ETag: \"%s\"\n", ctx.page.etag);
751         html("\n");
752         if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))
753                 exit(0);
754 }
755
756 void cgit_redirect(const char *url, bool permanent)
757 {
758         htmlf("Status: %d %s\n", permanent ? 301 : 302, permanent ? "Moved" : "Found");
759         html("Location: ");
760         html_url_path(url);
761         html("\n\n");
762 }
763
764 static void print_rel_vcs_link(const char *url)
765 {
766         html("<link rel='vcs-git' href='");
767         html_attr(url);
768         html("' title='");
769         html_attr(ctx.repo->name);
770         html(" Git repository'/>\n");
771 }
772
773 static int emit_css_link(struct string_list_item *s, void *arg)
774 {
775         /* Do not emit anything if css= is specified. */
776         if (s && *s->string == '\0')
777                 return 0;
778
779         html("<link rel='stylesheet' type='text/css' href='");
780         if (s)
781                 html_attr(s->string);
782         else
783                 html_attr((const char *)arg);
784         html("'/>\n");
785
786         return 0;
787 }
788
789 static int emit_js_link(struct string_list_item *s, void *arg)
790 {
791         /* Do not emit anything if js= is specified. */
792         if (s && *s->string == '\0')
793                 return 0;
794
795         html("<script type='text/javascript' src='");
796         if (s)
797                 html_attr(s->string);
798         else
799                 html_attr((const char *)arg);
800         html("'></script>\n");
801
802         return 0;
803 }
804
805 void cgit_print_docstart(void)
806 {
807         char *host = cgit_hosturl();
808
809         if (ctx.cfg.embedded) {
810                 if (ctx.cfg.header)
811                         html_include(ctx.cfg.header);
812                 return;
813         }
814
815         html(cgit_doctype);
816         html("<html lang='en'>\n");
817         html("<head>\n");
818         html("<title>");
819         html_txt(ctx.page.title);
820         html("</title>\n");
821         htmlf("<meta name='generator' content='cgit %s'/>\n", cgit_version);
822         if (ctx.cfg.robots && *ctx.cfg.robots)
823                 htmlf("<meta name='robots' content='%s'/>\n", ctx.cfg.robots);
824
825         if (ctx.cfg.css.items)
826                 for_each_string_list(&ctx.cfg.css, emit_css_link, NULL);
827         else
828                 emit_css_link(NULL, "/cgit.css");
829
830         if (ctx.cfg.js.items)
831                 for_each_string_list(&ctx.cfg.js, emit_js_link, NULL);
832         else
833                 emit_js_link(NULL, "/cgit.js");
834
835         if (ctx.cfg.favicon) {
836                 html("<link rel='shortcut icon' href='");
837                 html_attr(ctx.cfg.favicon);
838                 html("'/>\n");
839         }
840         if (host && ctx.repo && ctx.qry.head) {
841                 char *fileurl;
842                 struct strbuf sb = STRBUF_INIT;
843                 strbuf_addf(&sb, "h=%s", ctx.qry.head);
844
845                 html("<link rel='alternate' title='Atom feed' href='");
846                 html(cgit_httpscheme());
847                 html_attr(host);
848                 fileurl = cgit_fileurl(ctx.repo->url, "atom", ctx.qry.vpath,
849                                        sb.buf);
850                 html_attr(fileurl);
851                 html("' type='application/atom+xml'/>\n");
852                 strbuf_release(&sb);
853                 free(fileurl);
854         }
855         if (ctx.repo)
856                 cgit_add_clone_urls(print_rel_vcs_link);
857         if (ctx.cfg.head_include)
858                 html_include(ctx.cfg.head_include);
859         if (ctx.repo && ctx.repo->extra_head_content)
860                 html(ctx.repo->extra_head_content);
861         html("</head>\n");
862         html("<body>\n");
863         if (ctx.cfg.header)
864                 html_include(ctx.cfg.header);
865         free(host);
866 }
867
868 void cgit_print_docend(void)
869 {
870         html("</div> <!-- class=content -->\n");
871         if (ctx.cfg.embedded) {
872                 html("</div> <!-- id=cgit -->\n");
873                 if (ctx.cfg.footer)
874                         html_include(ctx.cfg.footer);
875                 return;
876         }
877         if (ctx.cfg.footer)
878                 html_include(ctx.cfg.footer);
879         else {
880                 htmlf("<div class='footer'>generated by <a href='https://git.zx2c4.com/cgit/about/'>cgit %s</a> "
881                         "(<a href='https://git-scm.com/'>git %s</a>) at ", cgit_version, git_version_string);
882                 html_txt(show_date(time(NULL), 0, cgit_date_mode(DATE_ISO8601)));
883                 html("</div>\n");
884         }
885         html("</div> <!-- id=cgit -->\n");
886         html("</body>\n</html>\n");
887 }
888
889 void cgit_print_error_page(int code, const char *msg, const char *fmt, ...)
890 {
891         va_list ap;
892         ctx.page.expires = ctx.cfg.cache_dynamic_ttl;
893         ctx.page.status = code;
894         ctx.page.statusmsg = msg;
895         cgit_print_layout_start();
896         va_start(ap, fmt);
897         cgit_vprint_error(fmt, ap);
898         va_end(ap);
899         cgit_print_layout_end();
900 }
901
902 void cgit_print_layout_start(void)
903 {
904         cgit_print_http_headers();
905         cgit_print_docstart();
906         cgit_print_pageheader();
907 }
908
909 void cgit_print_layout_end(void)
910 {
911         cgit_print_docend();
912 }
913
914 static void add_clone_urls(void (*fn)(const char *), char *txt, char *suffix)
915 {
916         struct strbuf **url_list = strbuf_split_str(txt, ' ', 0);
917         int i;
918
919         for (i = 0; url_list[i]; i++) {
920                 strbuf_rtrim(url_list[i]);
921                 if (url_list[i]->len == 0)
922                         continue;
923                 if (suffix && *suffix)
924                         strbuf_addf(url_list[i], "/%s", suffix);
925                 fn(url_list[i]->buf);
926         }
927
928         strbuf_list_free(url_list);
929 }
930
931 void cgit_add_clone_urls(void (*fn)(const char *))
932 {
933         if (ctx.repo->clone_url)
934                 add_clone_urls(fn, expand_macros(ctx.repo->clone_url), NULL);
935         else if (ctx.cfg.clone_prefix)
936                 add_clone_urls(fn, ctx.cfg.clone_prefix, ctx.repo->url);
937 }
938
939 static int print_branch_option(const char *refname, const struct object_id *oid,
940                                int flags, void *cb_data)
941 {
942         char *name = (char *)refname;
943         html_option(name, name, ctx.qry.head);
944         return 0;
945 }
946
947 void cgit_add_hidden_formfields(int incl_head, int incl_search,
948                                 const char *page)
949 {
950         if (!ctx.cfg.virtual_root) {
951                 struct strbuf url = STRBUF_INIT;
952
953                 strbuf_addf(&url, "%s/%s", ctx.qry.repo, page);
954                 if (ctx.qry.vpath)
955                         strbuf_addf(&url, "/%s", ctx.qry.vpath);
956                 html_hidden("url", url.buf);
957                 strbuf_release(&url);
958         }
959
960         if (incl_head && ctx.qry.head && ctx.repo->defbranch &&
961             strcmp(ctx.qry.head, ctx.repo->defbranch))
962                 html_hidden("h", ctx.qry.head);
963
964         if (ctx.qry.oid)
965                 html_hidden("id", ctx.qry.oid);
966         if (ctx.qry.oid2)
967                 html_hidden("id2", ctx.qry.oid2);
968         if (ctx.qry.showmsg)
969                 html_hidden("showmsg", "1");
970
971         if (incl_search) {
972                 if (ctx.qry.grep)
973                         html_hidden("qt", ctx.qry.grep);
974                 if (ctx.qry.search)
975                         html_hidden("q", ctx.qry.search);
976         }
977 }
978
979 static const char *hc(const char *page)
980 {
981         if (!ctx.qry.page)
982                 return NULL;
983
984         return strcmp(ctx.qry.page, page) ? NULL : "active";
985 }
986
987 static void cgit_print_path_crumbs(char *path)
988 {
989         char *old_path = ctx.qry.path;
990         char *p = path, *q, *end = path + strlen(path);
991         int levels = 0;
992
993         ctx.qry.path = NULL;
994         cgit_self_link("root", NULL, NULL);
995         ctx.qry.path = p = path;
996         while (p < end) {
997                 if (!(q = strchr(p, '/')) || levels > 15)
998                         q = end;
999                 *q = '\0';
1000                 html_txt("/");
1001                 cgit_self_link(p, NULL, NULL);
1002                 if (q < end)
1003                         *q = '/';
1004                 p = q + 1;
1005                 ++levels;
1006         }
1007         ctx.qry.path = old_path;
1008 }
1009
1010 static void print_header(void)
1011 {
1012         char *logo = NULL, *logo_link = NULL;
1013
1014         html("<table id='header'>\n");
1015         html("<tr>\n");
1016
1017         if (ctx.repo && ctx.repo->logo && *ctx.repo->logo)
1018                 logo = ctx.repo->logo;
1019         else
1020                 logo = ctx.cfg.logo;
1021         if (ctx.repo && ctx.repo->logo_link && *ctx.repo->logo_link)
1022                 logo_link = ctx.repo->logo_link;
1023         else
1024                 logo_link = ctx.cfg.logo_link;
1025         if (logo && *logo) {
1026                 html("<td class='logo' rowspan='2'><a href='");
1027                 if (logo_link && *logo_link)
1028                         html_attr(logo_link);
1029                 else
1030                         html_attr(cgit_rooturl());
1031                 html("'><img src='");
1032                 html_attr(logo);
1033                 html("' alt='cgit logo'/></a></td>\n");
1034         }
1035
1036         html("<td class='main'>");
1037         if (ctx.repo) {
1038                 cgit_index_link("index", NULL, NULL, NULL, NULL, 0, 1);
1039                 html(" : ");
1040                 cgit_summary_link(ctx.repo->name, NULL, NULL, NULL);
1041                 if (ctx.env.authenticated) {
1042                         html("</td><td class='form'>");
1043                         html("<form method='get'>\n");
1044                         cgit_add_hidden_formfields(0, 1, ctx.qry.page);
1045                         html("<select name='h' onchange='this.form.submit();'>\n");
1046                         refs_for_each_branch_ref(get_main_ref_store(the_repository),
1047                                                  print_branch_option, ctx.qry.head);
1048                         if (ctx.repo->enable_remote_branches)
1049                                 refs_for_each_remote_ref(get_main_ref_store(the_repository),
1050                                                          print_branch_option, ctx.qry.head);
1051                         html("</select> ");
1052                         html("<input type='submit' value='switch'/>");
1053                         html("</form>");
1054                 }
1055         } else
1056                 html_txt(ctx.cfg.root_title);
1057         html("</td></tr>\n");
1058
1059         html("<tr><td class='sub'>");
1060         if (ctx.repo) {
1061                 html_txt(ctx.repo->desc);
1062                 html("</td><td class='sub right'>");
1063                 if (ctx.repo->owner_filter) {
1064                         cgit_open_filter(ctx.repo->owner_filter);
1065                         html_txt(ctx.repo->owner);
1066                         cgit_close_filter(ctx.repo->owner_filter);
1067                 } else {
1068                         html_txt(ctx.repo->owner);
1069                 }
1070         } else {
1071                 if (ctx.cfg.root_desc)
1072                         html_txt(ctx.cfg.root_desc);
1073         }
1074         html("</td></tr></table>\n");
1075 }
1076
1077 void cgit_print_pageheader(void)
1078 {
1079         html("<div id='cgit'>");
1080         if (!ctx.env.authenticated || !ctx.cfg.noheader)
1081                 print_header();
1082
1083         html("<table class='tabs'><tr><td>\n");
1084         if (ctx.env.authenticated && ctx.repo) {
1085                 if (ctx.repo->readme.nr)
1086                         reporevlink("about", "about", NULL,
1087                                     hc("about"), ctx.qry.head, NULL,
1088                                     NULL);
1089                 cgit_summary_link("summary", NULL, hc("summary"),
1090                                   ctx.qry.head);
1091                 cgit_refs_link("refs", NULL, hc("refs"), ctx.qry.head,
1092                                ctx.qry.oid, NULL);
1093                 cgit_log_link("log", NULL, hc("log"), ctx.qry.head,
1094                               NULL, ctx.qry.vpath, 0, NULL, NULL,
1095                               ctx.qry.showmsg, ctx.qry.follow);
1096                 if (ctx.qry.page && !strcmp(ctx.qry.page, "blame"))
1097                         cgit_blame_link("blame", NULL, hc("blame"), ctx.qry.head,
1098                                         ctx.qry.oid, ctx.qry.vpath);
1099                 else
1100                         cgit_tree_link("tree", NULL, hc("tree"), ctx.qry.head,
1101                                        ctx.qry.oid, ctx.qry.vpath);
1102                 cgit_commit_link("commit", NULL, hc("commit"),
1103                                  ctx.qry.head, ctx.qry.oid, ctx.qry.vpath);
1104                 cgit_diff_link("diff", NULL, hc("diff"), ctx.qry.head,
1105                                ctx.qry.oid, ctx.qry.oid2, ctx.qry.vpath);
1106                 if (ctx.repo->max_stats)
1107                         cgit_stats_link("stats", NULL, hc("stats"),
1108                                         ctx.qry.head, ctx.qry.vpath);
1109                 if (ctx.repo->homepage) {
1110                         html("<a href='");
1111                         html_attr(ctx.repo->homepage);
1112                         html("'>homepage</a>");
1113                 }
1114                 html("</td><td class='form'>");
1115                 html("<form class='right' method='get' action='");
1116                 if (ctx.cfg.virtual_root) {
1117                         char *fileurl = cgit_fileurl(ctx.qry.repo, "log",
1118                                                    ctx.qry.vpath, NULL);
1119                         html_url_path(fileurl);
1120                         free(fileurl);
1121                 }
1122                 html("'>\n");
1123                 cgit_add_hidden_formfields(1, 0, "log");
1124                 html("<select name='qt'>\n");
1125                 html_option("grep", "log msg", ctx.qry.grep);
1126                 html_option("author", "author", ctx.qry.grep);
1127                 html_option("committer", "committer", ctx.qry.grep);
1128                 html_option("range", "range", ctx.qry.grep);
1129                 html("</select>\n");
1130                 html("<input class='txt' type='search' size='10' name='q' value='");
1131                 html_attr(ctx.qry.search);
1132                 html("'/>\n");
1133                 html("<input type='submit' value='search'/>\n");
1134                 html("</form>\n");
1135         } else if (ctx.env.authenticated) {
1136                 char *currenturl = cgit_currenturl();
1137                 site_link(NULL, "index", NULL, hc("repolist"), NULL, NULL, 0, 1);
1138                 if (ctx.cfg.root_readme)
1139                         site_link("about", "about", NULL, hc("about"),
1140                                   NULL, NULL, 0, 1);
1141                 html("</td><td class='form'>");
1142                 html("<form method='get' action='");
1143                 html_attr(currenturl);
1144                 html("'>\n");
1145                 html("<input type='search' name='q' size='10' value='");
1146                 html_attr(ctx.qry.search);
1147                 html("'/>\n");
1148                 html("<input type='submit' value='search'/>\n");
1149                 html("</form>");
1150                 free(currenturl);
1151         }
1152         html("</td></tr></table>\n");
1153         if (ctx.env.authenticated && ctx.repo && ctx.qry.vpath) {
1154                 html("<div class='path'>");
1155                 html("path: ");
1156                 cgit_print_path_crumbs(ctx.qry.vpath);
1157                 if (ctx.cfg.enable_follow_links && !strcmp(ctx.qry.page, "log")) {
1158                         html(" (");
1159                         ctx.qry.follow = !ctx.qry.follow;
1160                         cgit_self_link(ctx.qry.follow ? "follow" : "unfollow",
1161                                         NULL, NULL);
1162                         ctx.qry.follow = !ctx.qry.follow;
1163                         html(")");
1164                 }
1165                 html("</div>");
1166         }
1167         html("<div class='content'>");
1168 }
1169
1170 void cgit_print_filemode(unsigned short mode)
1171 {
1172         if (S_ISDIR(mode))
1173                 html("d");
1174         else if (S_ISLNK(mode))
1175                 html("l");
1176         else if (S_ISGITLINK(mode))
1177                 html("m");
1178         else
1179                 html("-");
1180         html_fileperm(mode >> 6);
1181         html_fileperm(mode >> 3);
1182         html_fileperm(mode);
1183 }
1184
1185 void cgit_compose_snapshot_prefix(struct strbuf *filename, const char *base,
1186                                   const char *ref)
1187 {
1188         struct object_id oid;
1189
1190         /*
1191          * Prettify snapshot names by stripping leading "v" or "V" if the tag
1192          * name starts with {v,V}[0-9] and the prettify mapping is injective,
1193          * i.e. each stripped tag can be inverted without ambiguities.
1194          */
1195         if (repo_get_oid(the_repository, fmt("refs/tags/%s", ref), &oid) == 0 &&
1196             (ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1]) &&
1197             ((repo_get_oid(the_repository, fmt("refs/tags/%s", ref + 1), &oid) == 0) +
1198              (repo_get_oid(the_repository, fmt("refs/tags/v%s", ref + 1), &oid) == 0) +
1199              (repo_get_oid(the_repository, fmt("refs/tags/V%s", ref + 1), &oid) == 0) == 1))
1200                 ref++;
1201
1202         strbuf_addf(filename, "%s-%s", base, ref);
1203 }
1204
1205 void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref,
1206                                const char *separator)
1207 {
1208         const struct cgit_snapshot_format *f;
1209         struct strbuf filename = STRBUF_INIT;
1210         const char *basename;
1211         size_t prefixlen;
1212
1213         basename = cgit_snapshot_prefix(repo);
1214         if (starts_with(ref, basename))
1215                 strbuf_addstr(&filename, ref);
1216         else
1217                 cgit_compose_snapshot_prefix(&filename, basename, ref);
1218
1219         prefixlen = filename.len;
1220         for (f = cgit_snapshot_formats; f->suffix; f++) {
1221                 if (!(repo->snapshots & cgit_snapshot_format_bit(f)))
1222                         continue;
1223                 strbuf_setlen(&filename, prefixlen);
1224                 strbuf_addstr(&filename, f->suffix);
1225                 cgit_snapshot_link(filename.buf, NULL, NULL, NULL, NULL,
1226                                    filename.buf);
1227                 if (cgit_snapshot_get_sig(ref, f)) {
1228                         strbuf_addstr(&filename, ".asc");
1229                         html(" (");
1230                         cgit_snapshot_link("sig", NULL, NULL, NULL, NULL,
1231                                            filename.buf);
1232                         html(")");
1233                 } else if (starts_with(f->suffix, ".tar") && cgit_snapshot_get_sig(ref, &cgit_snapshot_formats[0])) {
1234                         strbuf_setlen(&filename, strlen(filename.buf) - strlen(f->suffix));
1235                         strbuf_addstr(&filename, ".tar.asc");
1236                         html(" (");
1237                         cgit_snapshot_link("sig", NULL, NULL, NULL, NULL,
1238                                            filename.buf);
1239                         html(")");
1240                 }
1241                 html(separator);
1242         }
1243         strbuf_release(&filename);
1244 }
1245
1246 void cgit_set_title_from_path(const char *path)
1247 {
1248         struct strbuf sb = STRBUF_INIT;
1249         const char *slash, *last_slash;
1250
1251         if (!path)
1252                 return;
1253
1254         for (last_slash = path + strlen(path); (slash = memrchr(path, '/', last_slash - path)) != NULL; last_slash = slash) {
1255                 strbuf_add(&sb, slash + 1, last_slash - slash - 1);
1256                 strbuf_addstr(&sb, " \xc2\xab ");
1257         }
1258         strbuf_add(&sb, path, last_slash - path);
1259         strbuf_addf(&sb, " - %s", ctx.page.title);
1260         ctx.page.title = strbuf_detach(&sb, NULL);
1261 }