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, `"`, `&quot;`)
   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  }