1
// Copyright 2014 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
4
package v5 // import "gopkg.in/juju/charmstore.v5-unstable/internal/v5"
17
"gopkg.in/juju/charm.v6-unstable"
18
"gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
19
"gopkg.in/juju/jujusvg.v1"
21
"gopkg.in/juju/charmstore.v5-unstable/internal/charmstore"
22
"gopkg.in/juju/charmstore.v5-unstable/internal/mongodoc"
23
"gopkg.in/juju/charmstore.v5-unstable/internal/router"
27
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-iddiagramsvg
28
func (h *ReqHandler) serveDiagram(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
29
if id.URL.Series != "bundle" {
30
return errgo.WithCausef(nil, params.ErrNotFound, "diagrams not supported for charms")
32
entity, err := h.Cache.Entity(&id.URL, charmstore.FieldSelector("bundledata"))
34
return errgo.Mask(err, errgo.Is(params.ErrNotFound))
38
// TODO consider what happens when a charm's SVG does not exist.
39
canvas, err := jujusvg.NewFromBundle(entity.BundleData, func(id *charm.URL) string {
40
// TODO change jujusvg so that the iconURL function can
42
absPath := h.Handler.rootPath + "/" + id.Path() + "/icon.svg"
43
p, err := router.RelativeURLPath(req.RequestURI, absPath)
45
urlErr = errgo.Notef(err, "cannot make relative URL from %q and %q", req.RequestURI, absPath)
50
return errgo.Notef(err, "cannot create canvas")
55
setArchiveCacheControl(w.Header(), h.isPublic(id))
56
w.Header().Set("Content-Type", "image/svg+xml")
61
// These are all forms of README files
62
// actually observed in charms in the wild.
63
var allowedReadMe = map[string]bool{
68
"readme.markdown": true,
73
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idreadme
74
func (h *ReqHandler) serveReadMe(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
75
entity, err := h.Cache.Entity(&id.URL, charmstore.FieldSelector("contents", "blobname"))
77
return errgo.NoteMask(err, "cannot get README", errgo.Is(params.ErrNotFound))
79
isReadMeFile := func(f *zip.File) bool {
80
name := strings.ToLower(path.Clean(f.Name))
81
// This is the same condition currently used by the GUI.
82
// TODO propagate likely content type from file extension.
83
return allowedReadMe[name]
85
r, err := h.Store.OpenCachedBlobFile(entity, mongodoc.FileReadMe, isReadMeFile)
87
return errgo.Mask(err, errgo.Is(params.ErrNotFound))
90
setArchiveCacheControl(w.Header(), h.isPublic(id))
96
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idiconsvg
97
func (h *ReqHandler) serveIcon(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
98
if id.URL.Series == "bundle" {
99
return errgo.WithCausef(nil, params.ErrNotFound, "icons not supported for bundles")
101
entity, err := h.Cache.Entity(&id.URL, charmstore.FieldSelector("contents", "blobname"))
103
return errgo.NoteMask(err, "cannot get icon", errgo.Is(params.ErrNotFound))
105
isIconFile := func(f *zip.File) bool {
106
return path.Clean(f.Name) == "icon.svg"
108
r, err := h.Store.OpenCachedBlobFile(entity, mongodoc.FileIcon, isIconFile)
110
logger.Errorf("cannot open icon.svg file for %v: %v", id, err)
111
if errgo.Cause(err) != params.ErrNotFound {
112
return errgo.Mask(err)
114
setArchiveCacheControl(w.Header(), h.isPublic(id))
115
w.Header().Set("Content-Type", "image/svg+xml")
116
io.Copy(w, strings.NewReader(DefaultIcon))
120
w.Header().Set("Content-Type", "image/svg+xml")
121
setArchiveCacheControl(w.Header(), h.isPublic(id))
122
if err := processIcon(w, r); err != nil {
123
if errgo.Cause(err) == errProbablyNotXML {
124
logger.Errorf("cannot process icon.svg from %s: %v", id, err)
125
io.Copy(w, strings.NewReader(DefaultIcon))
128
return errgo.Mask(err)
133
var errProbablyNotXML = errgo.New("probably not XML")
135
const svgNamespace = "http://www.w3.org/2000/svg"
137
// processIcon reads an icon SVG from r and writes
138
// it to w, making any changes that need to be made.
139
// Currently it adds a viewBox attribute to the <svg>
140
// element if necessary.
141
// If there is an error processing the XML before
142
// the first token has been written, it returns an error
143
// with errProbablyNotXML as the cause.
144
func processIcon(w io.Writer, r io.Reader) error {
145
// Arrange to save all the content that we find up
146
// until the first <svg> element. Then we'll stitch it
147
// back together again for the actual processing.
148
var saved bytes.Buffer
149
dec := xml.NewDecoder(io.TeeReader(r, &saved))
150
dec.DefaultSpace = svgNamespace
151
found, changed := false, false
153
tok, err := dec.Token()
158
return errgo.WithCausef(err, errProbablyNotXML, "")
160
_, found, changed = ensureViewbox(tok)
163
return errgo.WithCausef(nil, errProbablyNotXML, "no <svg> element found")
165
// Stitch the input back together again so we can
166
// write the output without buffering it in memory.
167
r = io.MultiReader(&saved, r)
168
if !found || !changed {
169
_, err := io.Copy(w, r)
172
return processNaive(w, r)
175
// processNaive is like processIcon but processes all of the
176
// XML elements. It does not return errProbablyNotXML
177
// on error because it may have written arbitrary XML
178
// to w, at which point writing an alternative response would
180
func processNaive(w io.Writer, r io.Reader) error {
181
dec := xml.NewDecoder(r)
182
dec.DefaultSpace = svgNamespace
183
enc := xml.NewEncoder(w)
186
tok, err := dec.Token()
191
return fmt.Errorf("failed to read token: %v", err)
194
tok, found, _ = ensureViewbox(tok)
196
if err := enc.EncodeToken(tok); err != nil {
197
return fmt.Errorf("cannot encode token %#v: %v", tok, err)
200
if err := enc.Flush(); err != nil {
201
return fmt.Errorf("cannot flush output: %v", err)
206
func ensureViewbox(tok0 xml.Token) (_ xml.Token, found, changed bool) {
207
tok, ok := tok0.(xml.StartElement)
208
if !ok || tok.Name.Space != svgNamespace || tok.Name.Local != "svg" {
209
return tok0, false, false
211
var width, height string
212
for _, attr := range tok.Attr {
213
if attr.Name.Space != "" {
216
switch attr.Name.Local {
222
return tok, true, false
225
if width == "" || height == "" {
226
// Width and/or height have not been specified,
227
// so leave viewbox unspecified too.
228
return tok, true, false
230
tok.Attr = append(tok.Attr, xml.Attr{
234
Value: fmt.Sprintf("0 0 %s %s", width, height),
236
return tok, true, true