// Copyright 2015 The Gogs Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package repo import ( "fmt" "io/ioutil" "net/url" "path/filepath" "strings" "time" "code.gitea.io/git" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/markdown" "code.gitea.io/gitea/modules/markup" ) const ( tplWikiStart base.TplName = "repo/wiki/start" tplWikiView base.TplName = "repo/wiki/view" tplWikiNew base.TplName = "repo/wiki/new" tplWikiPages base.TplName = "repo/wiki/pages" ) // MustEnableWiki check if wiki is enabled, if external then redirect func MustEnableWiki(ctx *context.Context) { if !ctx.Repo.Repository.UnitEnabled(models.UnitTypeWiki) && !ctx.Repo.Repository.UnitEnabled(models.UnitTypeExternalWiki) { ctx.Handle(404, "MustEnableWiki", nil) return } unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalWiki) if err == nil { ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL) return } } // PageMeta wiki page meat information type PageMeta struct { Name string URL string Updated time.Time } func urlEncoded(str string) string { u, err := url.Parse(str) if err != nil { return str } return u.String() } func urlDecoded(str string) string { res, err := url.QueryUnescape(str) if err != nil { return str } return res } // commitTreeBlobEntry processes found file and checks if it matches search target func commitTreeBlobEntry(entry *git.TreeEntry, path string, targets []string, textOnly bool) *git.TreeEntry { name := entry.Name() ext := filepath.Ext(name) if !textOnly || markdown.IsMarkdownFile(name) || ext == ".textile" { for _, target := range targets { if matchName(path, target) || matchName(urlEncoded(path), target) || matchName(urlDecoded(path), target) { return entry } pathNoExt := strings.TrimSuffix(path, ext) if matchName(pathNoExt, target) || matchName(urlEncoded(pathNoExt), target) || matchName(urlDecoded(pathNoExt), target) { return entry } } } return nil } // commitTreeDirEntry is a recursive file tree traversal function func commitTreeDirEntry(repo *git.Repository, commit *git.Commit, entries []*git.TreeEntry, prevPath string, targets []string, textOnly bool) (*git.TreeEntry, error) { for i := range entries { entry := entries[i] var path string if len(prevPath) == 0 { path = entry.Name() } else { path = prevPath + "/" + entry.Name() } if entry.Type == git.ObjectBlob { // File if res := commitTreeBlobEntry(entry, path, targets, textOnly); res != nil { return res, nil } } else if entry.IsDir() { // Directory // Get our tree entry, handling all possible errors var err error var tree *git.Tree if tree, err = repo.GetTree(entry.ID.String()); tree == nil || err != nil { if err == nil { err = fmt.Errorf("repo.GetTree(%s) => nil", entry.ID.String()) } return nil, err } // Found us, get children entries var ls git.Entries if ls, err = tree.ListEntries(); err != nil { return nil, err } // Call itself recursively to find needed entry var te *git.TreeEntry if te, err = commitTreeDirEntry(repo, commit, ls, path, targets, textOnly); err != nil { return nil, err } if te != nil { return te, nil } } } return nil, nil } // commitTreeEntry is a first step of commitTreeDirEntry, which should be never called directly func commitTreeEntry(repo *git.Repository, commit *git.Commit, targets []string, textOnly bool) (*git.TreeEntry, error) { entries, err := commit.ListEntries() if err != nil { return nil, err } return commitTreeDirEntry(repo, commit, entries, "", targets, textOnly) } // findFile finds the best match for given filename in repo file tree func findFile(repo *git.Repository, commit *git.Commit, target string, textOnly bool) (*git.TreeEntry, error) { targets := []string{target, urlEncoded(target), urlDecoded(target)} var entry *git.TreeEntry var err error if entry, err = commitTreeEntry(repo, commit, targets, textOnly); err != nil { return nil, err } return entry, nil } // matchName matches generic name representation of the file with required one func matchName(target, name string) bool { if len(target) != len(name) { return false } name = strings.ToLower(name) target = strings.ToLower(target) if name == target { return true } target = strings.Replace(target, " ", "?", -1) target = strings.Replace(target, "-", "?", -1) for i := range name { ch := name[i] reqCh := target[i] if ch != reqCh { if string(reqCh) != "?" { return false } } } return true } func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) { wikiRepo, err := git.OpenRepository(ctx.Repo.Repository.WikiPath()) if err != nil { // ctx.Handle(500, "OpenRepository", err) return nil, nil, err } if !wikiRepo.IsBranchExist("master") { return wikiRepo, nil, nil } commit, err := wikiRepo.GetBranchCommit("master") if err != nil { ctx.Handle(500, "GetBranchCommit", err) return wikiRepo, nil, err } return wikiRepo, commit, nil } func renderWikiPage(ctx *context.Context, isViewPage bool) (*git.Repository, *git.TreeEntry) { wikiRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { return nil, nil } if commit == nil { return wikiRepo, nil } // Get page list. if isViewPage { entries, err := commit.ListEntries() if err != nil { ctx.Handle(500, "ListEntries", err) return nil, nil } pages := []PageMeta{} for i := range entries { if entries[i].Type == git.ObjectBlob { name := entries[i].Name() ext := filepath.Ext(name) if markdown.IsMarkdownFile(name) || ext == ".textile" { name = strings.TrimSuffix(name, ext) if name == "" || name == "_Sidebar" || name == "_Footer" || name == "_Header" { continue } pages = append(pages, PageMeta{ Name: models.ToWikiPageName(name), URL: name, }) } } } ctx.Data["Pages"] = pages } pageURL := ctx.Params(":page") if len(pageURL) == 0 { pageURL = "Home" } ctx.Data["PageURL"] = pageURL pageName := models.ToWikiPageName(pageURL) ctx.Data["old_title"] = pageName ctx.Data["Title"] = pageName ctx.Data["title"] = pageName ctx.Data["RequireHighlightJS"] = true var entry *git.TreeEntry if entry, err = findFile(wikiRepo, commit, pageName, true); err != nil { ctx.Handle(500, "findFile", err) return nil, nil } if entry == nil { ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages") return nil, nil } blob := entry.Blob() r, err := blob.Data() if err != nil { ctx.Handle(500, "Data", err) return nil, nil } data, err := ioutil.ReadAll(r) if err != nil { ctx.Handle(500, "ReadAll", err) return nil, nil } sidebarPresent := false sidebarContent := []byte{} sentry, err := findFile(wikiRepo, commit, "_Sidebar", true) if err == nil && sentry != nil { r, err = sentry.Blob().Data() if err == nil { dataSB, err := ioutil.ReadAll(r) if err == nil { sidebarPresent = true sidebarContent = dataSB } } } footerPresent := false footerContent := []byte{} sentry, err = findFile(wikiRepo, commit, "_Footer", true) if err == nil && sentry != nil { r, err = sentry.Blob().Data() if err == nil { dataSB, err := ioutil.ReadAll(r) if err == nil { footerPresent = true footerContent = dataSB } } } if isViewPage { metas := ctx.Repo.Repository.ComposeMetas() ctx.Data["content"] = markdown.RenderWiki(data, ctx.Repo.RepoLink, metas) ctx.Data["sidebarPresent"] = sidebarPresent ctx.Data["sidebarContent"] = markdown.RenderWiki(sidebarContent, ctx.Repo.RepoLink, metas) ctx.Data["footerPresent"] = footerPresent ctx.Data["footerContent"] = markdown.RenderWiki(footerContent, ctx.Repo.RepoLink, metas) } else { ctx.Data["content"] = string(data) ctx.Data["sidebarPresent"] = false ctx.Data["sidebarContent"] = "" ctx.Data["footerPresent"] = false ctx.Data["footerContent"] = "" } return wikiRepo, entry } // Wiki renders single wiki page func Wiki(ctx *context.Context) { ctx.Data["PageIsWiki"] = true if !ctx.Repo.Repository.HasWiki() { ctx.Data["Title"] = ctx.Tr("repo.wiki") ctx.HTML(200, tplWikiStart) return } wikiRepo, entry := renderWikiPage(ctx, true) if ctx.Written() { return } if entry == nil { ctx.Data["Title"] = ctx.Tr("repo.wiki") ctx.HTML(200, tplWikiStart) return } ename := entry.Name() if markup.Type(ename) != markdown.MarkupName { ext := strings.ToUpper(filepath.Ext(ename)) ctx.Data["FormatWarning"] = fmt.Sprintf("%s rendering is not supported at the moment. Rendered as Markdown.", ext) } // Get last change information. lastCommit, err := wikiRepo.GetCommitByPath(ename) if err != nil { ctx.Handle(500, "GetCommitByPath", err) return } ctx.Data["Author"] = lastCommit.Author ctx.HTML(200, tplWikiView) } // WikiPages render wiki pages list page func WikiPages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.wiki.pages") ctx.Data["PageIsWiki"] = true if !ctx.Repo.Repository.HasWiki() { ctx.Redirect(ctx.Repo.RepoLink + "/wiki") return } wikiRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { return } entries, err := commit.ListEntries() if err != nil { ctx.Handle(500, "ListEntries", err) return } pages := make([]PageMeta, 0, len(entries)) for i := range entries { if entries[i].Type == git.ObjectBlob { c, err := wikiRepo.GetCommitByPath(entries[i].Name()) if err != nil { ctx.Handle(500, "GetCommit", err) return } name := entries[i].Name() ext := filepath.Ext(name) if markdown.IsMarkdownFile(name) || ext == ".textile" { name = strings.TrimSuffix(name, ext) if name == "" { continue } pages = append(pages, PageMeta{ Name: models.ToWikiPageName(name), URL: name, Updated: c.Author.When, }) } } } ctx.Data["Pages"] = pages ctx.HTML(200, tplWikiPages) } // WikiRaw outputs raw blob requested by user (image for example) func WikiRaw(ctx *context.Context) { wikiRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { if wikiRepo != nil { return } } uri := ctx.Params("*") var entry *git.TreeEntry if commit != nil { entry, err = findFile(wikiRepo, commit, uri, false) } if err != nil || entry == nil { if entry == nil || commit == nil { defBranch := ctx.Repo.Repository.DefaultBranch if commit, err = ctx.Repo.GitRepo.GetBranchCommit(defBranch); commit == nil || err != nil { ctx.Handle(500, "GetBranchCommit", err) return } if entry, err = findFile(ctx.Repo.GitRepo, commit, uri, false); err != nil { ctx.Handle(500, "findFile", err) return } if entry == nil { ctx.Handle(404, "findFile", nil) return } } else { ctx.Handle(500, "findFile", err) return } } if err = ServeBlob(ctx, entry.Blob()); err != nil { ctx.Handle(500, "ServeBlob", err) } } // NewWiki render wiki create page func NewWiki(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page") ctx.Data["PageIsWiki"] = true ctx.Data["RequireSimpleMDE"] = true if !ctx.Repo.Repository.HasWiki() { ctx.Data["title"] = "Home" } ctx.HTML(200, tplWikiNew) } // NewWikiPost response fro wiki create request func NewWikiPost(ctx *context.Context, form auth.NewWikiForm) { ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page") ctx.Data["PageIsWiki"] = true ctx.Data["RequireSimpleMDE"] = true if ctx.HasError() { ctx.HTML(200, tplWikiNew) return } wikiPath := models.ToWikiPageURL(form.Title) if err := ctx.Repo.Repository.AddWikiPage(ctx.User, wikiPath, form.Content, form.Message); err != nil { if models.IsErrWikiAlreadyExist(err) { ctx.Data["Err_Title"] = true ctx.RenderWithErr(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form) } else { ctx.Handle(500, "AddWikiPage", err) } return } ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wikiPath) } // EditWiki render wiki modify page func EditWiki(ctx *context.Context) { ctx.Data["PageIsWiki"] = true ctx.Data["PageIsWikiEdit"] = true ctx.Data["RequireSimpleMDE"] = true if !ctx.Repo.Repository.HasWiki() { ctx.Redirect(ctx.Repo.RepoLink + "/wiki") return } renderWikiPage(ctx, false) if ctx.Written() { return } ctx.HTML(200, tplWikiNew) } // EditWikiPost response fro wiki modify request func EditWikiPost(ctx *context.Context, form auth.NewWikiForm) { ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page") ctx.Data["PageIsWiki"] = true ctx.Data["RequireSimpleMDE"] = true if ctx.HasError() { ctx.HTML(200, tplWikiNew) return } oldWikiPath := models.ToWikiPageURL(ctx.Params(":page")) newWikiPath := models.ToWikiPageURL(form.Title) if err := ctx.Repo.Repository.EditWikiPage(ctx.User, oldWikiPath, newWikiPath, form.Content, form.Message); err != nil { ctx.Handle(500, "EditWikiPage", err) return } ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + newWikiPath) } // DeleteWikiPagePost delete wiki page func DeleteWikiPagePost(ctx *context.Context) { pageURL := models.ToWikiPageURL(ctx.Params(":page")) if len(pageURL) == 0 { pageURL = "Home" } if err := ctx.Repo.Repository.DeleteWikiPage(ctx.User, pageURL); err != nil { ctx.Handle(500, "DeleteWikiPage", err) return } ctx.JSON(200, map[string]interface{}{ "redirect": ctx.Repo.RepoLink + "/wiki/", }) }