github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/spyglass/lenses/html/html.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package html 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "path/filepath" 24 "strings" 25 "text/template" 26 27 "golang.org/x/net/html" 28 29 "github.com/sirupsen/logrus" 30 31 "sigs.k8s.io/prow/pkg/config" 32 "sigs.k8s.io/prow/pkg/spyglass/api" 33 "sigs.k8s.io/prow/pkg/spyglass/lenses" 34 ) 35 36 func init() { 37 lenses.RegisterLens(Lens{}) 38 } 39 40 type Lens struct{} 41 42 type document struct { 43 Filename string 44 ID string 45 Title string 46 Content string 47 } 48 49 // Config returns the lens's configuration. 50 func (lens Lens) Config() lenses.LensConfig { 51 return lenses.LensConfig{ 52 Name: "html", 53 Title: "HTML", 54 Priority: 3, 55 HideTitle: true, 56 } 57 } 58 59 // Header renders the content of <head> from template.html. 60 func (lens Lens) Header(artifacts []api.Artifact, resourceDir string, config json.RawMessage, spyglassConfig config.Spyglass) string { 61 t, err := template.ParseFiles(filepath.Join(resourceDir, "template.html")) 62 if err != nil { 63 return fmt.Sprintf("<!-- FAILED LOADING HEADER: %v -->", err) 64 } 65 var buf bytes.Buffer 66 if err := t.ExecuteTemplate(&buf, "header", nil); err != nil { 67 return fmt.Sprintf("<!-- FAILED EXECUTING HEADER TEMPLATE: %v -->", err) 68 } 69 return buf.String() 70 } 71 72 // Callback does nothing. 73 func (lens Lens) Callback(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string { 74 return "" 75 } 76 77 // Body renders the <body> 78 func (lens Lens) Body(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string { 79 if len(artifacts) == 0 { 80 logrus.Error("html Body() called with no artifacts, which should never happen.") 81 return "Why am I here? There is no html file" 82 } 83 84 documents := make([]document, 0) 85 for i, artifact := range artifacts { 86 content, err := artifact.ReadAll() 87 if err != nil { 88 logrus.WithError(err).WithField("artifact_url", artifact.CanonicalLink()).Warn("failed to read content") 89 continue 90 } 91 name := filepath.Base(artifact.CanonicalLink()) 92 documents = append(documents, extractDocumentDetails(name, i, content)) 93 } 94 95 template, err := template.ParseFiles(filepath.Join(resourceDir, "template.html")) 96 if err != nil { 97 logrus.WithError(err).Error("Error executing template.") 98 return fmt.Sprintf("Failed to load template file: %v", err) 99 } 100 101 buf := &bytes.Buffer{} 102 if err := template.ExecuteTemplate(buf, "body", documents); err != nil { 103 return fmt.Sprintf("failed to execute template: %v", err) 104 } 105 return buf.String() 106 } 107 108 // extractDocumentDetails parses the HTML to extract the title and 109 // meta description tag, if present. 110 func extractDocumentDetails(name string, id int, content []byte) document { 111 doc := document{ 112 Filename: name, 113 ID: fmt.Sprintf("%s-%d", name, id), 114 Title: name, 115 Content: string(content), 116 } 117 118 description := "" 119 token := html.NewTokenizer(bytes.NewReader(content)) 120 isTitle := false 121 for { 122 switch token.Next() { 123 case html.ErrorToken: 124 doc.Content = injectHeightNotifier(doc.Content, doc.ID) 125 // Escape double quotes as we are going to put this into an iframes srcdoc attribute. We can not reference the 126 // src directly because we have to inject the height notifier. 127 // Ref: https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-srcdoc 128 doc.Content = strings.ReplaceAll(doc.Content, `"`, `"`) 129 130 if description != "" { 131 doc.Title = doc.Title + fmt.Sprintf(` <abbr class="icon material-icons" title="%s">info</abbr>`, description) 132 } 133 134 return doc 135 case html.StartTagToken, html.SelfClosingTagToken: 136 tt := token.Token() 137 switch tt.Data { 138 case "title": 139 isTitle = true 140 case "meta": 141 content := "" 142 isDescription := false 143 for _, attr := range tt.Attr { 144 if attr.Key == "name" && attr.Val == "description" { 145 isDescription = true 146 } else if attr.Key == "content" { 147 content = attr.Val 148 } 149 } 150 if isDescription { 151 description = content 152 } 153 } 154 case html.TextToken: 155 if isTitle { 156 isTitle = false 157 tt := token.Token() 158 if tt.Data != "" { 159 doc.Title = tt.Data 160 } 161 } 162 } 163 } 164 } 165 166 // injectHeightNotifier injects a small javascript snippet that will tell the iframe container about the height 167 // of the iframe. Iframe height can only be set as an absolute value and CORS doesn't allow the container to 168 // query the iframe. 169 func injectHeightNotifier(content string, id string) string { 170 return `<div id="wrapper">` + content + fmt.Sprintf(`</div><script type="text/javascript"> 171 window.addEventListener("load", function(){ 172 if(window.self === window.top) return; // if w.self === w.top, we are not in an iframe 173 send_height_to_parent_function = function(){ 174 var height = document.getElementById("wrapper").offsetHeight; 175 parent.postMessage({"height" : height , "id": "%s"}, "*"); 176 } 177 send_height_to_parent_function(); //whenever the page is loaded 178 window.addEventListener("resize", send_height_to_parent_function); // whenever the page is resized 179 var observer = new MutationObserver(send_height_to_parent_function); // whenever DOM changes PT1 180 var config = { attributes: true, childList: true, characterData: true, subtree:true}; // PT2 181 observer.observe(window.document, config); // PT3 182 }); 183 </script>`, id) 184 }