github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/routes/api.go (about)

     1  package routes
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"net/http"
     7  	"strconv"
     8  	"strings"
     9  
    10  	c "github.com/Azareal/Gosora/common"
    11  )
    12  
    13  // TODO: Make this a static file somehow? Is it possible for us to put this file somewhere else?
    14  // TODO: Add an API so that plugins can register disallowed areas. E.g. /guilds/join for plugin_guilds
    15  func RobotsTxt(w http.ResponseWriter, r *http.Request) c.RouteError {
    16  	// TODO: Do we have to put * or something at the end of the paths?
    17  	_, _ = w.Write([]byte(`User-agent: *
    18  Disallow: /panel/*
    19  Disallow: /topics/create/
    20  Disallow: /user/edit/*
    21  Disallow: /accounts/*
    22  Disallow: /report/*
    23  `))
    24  	return nil
    25  }
    26  
    27  var sitemapPageCap = 40000 // 40k, bump it up to 50k once we gzip this? Does brotli work on sitemaps?
    28  
    29  func writeXMLHeader(w http.ResponseWriter, r *http.Request) {
    30  	w.Header().Set("Content-Type", "application/xml")
    31  	w.Write([]byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"))
    32  }
    33  
    34  // TODO: Keep track of when a sitemap was last modifed and add a lastmod element for it
    35  func SitemapXml(w http.ResponseWriter, r *http.Request) c.RouteError {
    36  	var s string
    37  	if c.Config.SslSchema {
    38  		s = "s"
    39  	}
    40  	sitemapItem := func(path string) {
    41  		w.Write([]byte(`<sitemap>
    42  	<loc>http` + s + `://` + c.Site.URL + "/" + path + `</loc>
    43  </sitemap>
    44  `))
    45  	}
    46  	writeXMLHeader(w, r)
    47  	w.Write([]byte("<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"))
    48  	sitemapItem("sitemaps/topics.xml")
    49  	//sitemapItem("sitemaps/forums.xml")
    50  	//sitemapItem("sitemaps/users.xml")
    51  	w.Write([]byte("</sitemapindex>"))
    52  
    53  	return nil
    54  }
    55  
    56  type FuzzyRoute struct {
    57  	Path   string
    58  	Handle func(http.ResponseWriter, *http.Request, int) c.RouteError
    59  }
    60  
    61  // TODO: Add a sitemap API and clean things up
    62  // TODO: ^-- Make sure that the API is concurrent
    63  // TODO: Add a social group sitemap
    64  var sitemapRoutes = map[string]func(http.ResponseWriter, *http.Request) c.RouteError{
    65  	"forums.xml": SitemapForums,
    66  	"topics.xml": SitemapTopics,
    67  }
    68  
    69  // TODO: Use a router capable of parsing this rather than hard-coding the logic in
    70  var fuzzySitemapRoutes = map[string]FuzzyRoute{
    71  	"topics_page_": {"topics_page_(%d).xml", SitemapTopic},
    72  }
    73  
    74  func sitemapSwitch(w http.ResponseWriter, r *http.Request) c.RouteError {
    75  	path := r.URL.Path[len("/sitemaps/"):]
    76  	for name, fuzzy := range fuzzySitemapRoutes {
    77  		if strings.HasPrefix(path, name) && strings.HasSuffix(path, ".xml") {
    78  			spath := strings.TrimPrefix(path, name)
    79  			spath = strings.TrimSuffix(spath, ".xml")
    80  			page, err := strconv.Atoi(spath)
    81  			if err != nil {
    82  				// ? What's this? Do we need it? Was it just a quick trace?
    83  				c.DebugLogf("Unable to convert string '%s' to integer in fuzzy route", spath)
    84  				return c.NotFound(w, r, nil)
    85  			}
    86  			return fuzzy.Handle(w, r, page)
    87  		}
    88  	}
    89  
    90  	route, ok := sitemapRoutes[path]
    91  	if !ok {
    92  		return c.NotFound(w, r, nil)
    93  	}
    94  	return route(w, r)
    95  }
    96  
    97  func SitemapForums(w http.ResponseWriter, r *http.Request) c.RouteError {
    98  	var s string
    99  	if c.Config.SslSchema {
   100  		s = "s"
   101  	}
   102  	sitemapItem := func(path string) {
   103  		w.Write([]byte(`<url>
   104  	<loc>http` + s + `://` + c.Site.URL + path + `</loc>
   105  </url>
   106  `))
   107  	}
   108  
   109  	group, err := c.Groups.Get(c.GuestUser.Group)
   110  	if err != nil {
   111  		return c.SilentInternalErrorXML(errors.New("The guest group doesn't exist for some reason"), w, r)
   112  	}
   113  
   114  	writeXMLHeader(w, r)
   115  	w.Write([]byte("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"))
   116  
   117  	for _, fid := range group.CanSee {
   118  		// Avoid data races by copying the struct into something we can freely mold without worrying about breaking something somewhere else
   119  		f := c.Forums.DirtyGet(fid).Copy()
   120  		if f.ParentID == 0 && f.Name != "" && f.Active {
   121  			sitemapItem(c.BuildForumURL(c.NameToSlug(f.Name), f.ID))
   122  		}
   123  	}
   124  
   125  	w.Write([]byte("</urlset>"))
   126  	return nil
   127  }
   128  
   129  // TODO: Add a global ratelimit. 10 50MB files (smaller if compressed better) per minute?
   130  // ? We might have problems with banned users, if they have fewer ViewTopic permissions than guests as they'll be able to see this list. Then again, a banned user could just logout to see it
   131  func SitemapTopics(w http.ResponseWriter, r *http.Request) c.RouteError {
   132  	var s string
   133  	if c.Config.SslSchema {
   134  		s = "s"
   135  	}
   136  	sitemapItem := func(path string) {
   137  		w.Write([]byte(`<sitemap>
   138  	<loc>http` + s + `://` + c.Site.URL + "/" + path + `</loc>
   139  </sitemap>
   140  `))
   141  	}
   142  
   143  	group, err := c.Groups.Get(c.GuestUser.Group)
   144  	if err != nil {
   145  		return c.SilentInternalErrorXML(errors.New("The guest group doesn't exist for some reason"), w, r)
   146  	}
   147  
   148  	var visibleForums []c.Forum
   149  	for _, fid := range group.CanSee {
   150  		forum := c.Forums.DirtyGet(fid)
   151  		if forum.Name != "" && forum.Active {
   152  			visibleForums = append(visibleForums, forum.Copy())
   153  		}
   154  	}
   155  
   156  	topicCount, err := c.TopicCountInForums(visibleForums)
   157  	if err != nil {
   158  		return c.InternalErrorXML(err, w, r)
   159  	}
   160  
   161  	pageCount := topicCount / sitemapPageCap
   162  	//log.Print("topicCount", topicCount)
   163  	//log.Print("pageCount", pageCount)
   164  	writeXMLHeader(w, r)
   165  	w.Write([]byte("<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"))
   166  	for i := 0; i <= pageCount; i++ {
   167  		sitemapItem("sitemaps/topics_page_" + strconv.Itoa(i) + ".xml")
   168  	}
   169  	w.Write([]byte("</sitemapindex>"))
   170  	return nil
   171  }
   172  
   173  func SitemapTopic(w http.ResponseWriter, r *http.Request, page int) c.RouteError {
   174  	/*var s string
   175  	if c.Config.SslSchema {
   176  		s = "s"
   177  	}
   178  	var sitemapItem = func(path string) {
   179  			w.Write([]byte(`<url>
   180  		<loc>http` + s + `://` + c.Site.URL + "/" + path + `</loc>
   181  	</url>
   182  	`))
   183  		}*/
   184  
   185  	group, err := c.Groups.Get(c.GuestUser.Group)
   186  	if err != nil {
   187  		return c.SilentInternalErrorXML(errors.New("The guest group doesn't exist for some reason"), w, r)
   188  	}
   189  
   190  	var visibleForums []c.Forum
   191  	for _, fid := range group.CanSee {
   192  		forum := c.Forums.DirtyGet(fid)
   193  		if forum.Name != "" && forum.Active {
   194  			visibleForums = append(visibleForums, forum.Copy())
   195  		}
   196  	}
   197  
   198  	argList, qlist := c.ForumListToArgQ(visibleForums)
   199  	topicCount, err := c.ArgQToTopicCount(argList, qlist)
   200  	if err != nil {
   201  		return c.InternalErrorXML(err, w, r)
   202  	}
   203  
   204  	pageCount := topicCount / sitemapPageCap
   205  	//log.Print("topicCount", topicCount)
   206  	//log.Print("pageCount", pageCount)
   207  	//log.Print("page",page)
   208  	if page > pageCount {
   209  		page = pageCount
   210  	}
   211  
   212  	writeXMLHeader(w, r)
   213  	w.Write([]byte("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"))
   214  
   215  	w.Write([]byte("</urlset>"))
   216  	return nil
   217  }
   218  
   219  func SitemapUsers(w http.ResponseWriter, r *http.Request) c.RouteError {
   220  	writeXMLHeader(w, r)
   221  	w.Write([]byte("<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"))
   222  	return nil
   223  }
   224  
   225  type JsonMe struct {
   226  	User *c.MeUser
   227  	Site MeSite
   228  }
   229  
   230  // We don't want to expose too much information about the site, so we'll make this a small subset of c.site
   231  type MeSite struct {
   232  	MaxReqSize   int
   233  	StaticPrefix string
   234  }
   235  
   236  // APIMe returns information about the current logged-in user
   237  // TODO: Find some way to stop intermediaries from doing compression to avoid the BREACH attack
   238  // TODO: Decouple site settings into a different API? I'd like to avoid having too many requests, if possible, maybe we can use a different name for this?
   239  func APIMe(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError {
   240  	// TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats
   241  	h := w.Header()
   242  	h.Set("Content-Type", "application/json")
   243  	// We don't want an intermediary accidentally caching this
   244  	// TODO: Use this header anywhere with a user check?
   245  	h.Set("Cache-Control", "private")
   246  
   247  	me := JsonMe{u.Me(), MeSite{c.Site.MaxRequestSize, c.StaticFiles.Prefix}}
   248  	jsonBytes, err := json.Marshal(me)
   249  	if err != nil {
   250  		return c.InternalErrorJS(err, w, r)
   251  	}
   252  	w.Write(jsonBytes)
   253  
   254  	return nil
   255  }
   256  
   257  func OpenSearchXml(w http.ResponseWriter, r *http.Request) c.RouteError {
   258  	w.Header().Set("Content-Type", "application/xml")
   259  	furl := "http"
   260  	if c.Config.SslSchema {
   261  		furl += "s"
   262  	}
   263  	furl += "://" + c.Site.URL
   264  	w.Write([]byte(`<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
   265  	<ShortName>` + c.Site.Name + `</ShortName>
   266  	<InputEncoding>UTF-8</InputEncoding>
   267  	<Url type="text/html" template="` + furl + `/topics/?q={searchTerms}"/>
   268  	<Url type="application/opensearchdescription+xml" rel="self" template="` + furl + `/opensearch.xml"/>
   269  	<moz:SearchForm>` + furl + `</moz:SearchForm>
   270  </OpenSearchDescription>`))
   271  	return nil
   272  }