github.com/symfony-cli/symfony-cli@v0.0.0-20240514161054-ece2df437dfa/local/php/toolbar.go (about) 1 /* 2 * Copyright (c) 2021-present Fabien Potencier <fabien@symfony.com> 3 * 4 * This file is part of Symfony CLI project 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU Affero General Public License as 8 * published by the Free Software Foundation, either version 3 of the 9 * License, or (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU Affero General Public License for more details. 15 * 16 * You should have received a copy of the GNU Affero General Public License 17 * along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package php 21 22 import ( 23 "bytes" 24 "fmt" 25 "io" 26 "mime" 27 "net/http" 28 "regexp" 29 30 "github.com/pkg/errors" 31 "github.com/symfony-cli/symfony-cli/envs" 32 "github.com/symfony-cli/terminal" 33 "golang.org/x/text/cases" 34 "golang.org/x/text/language" 35 ) 36 37 func (p *Server) processToolbarInResponse(resp *http.Response) (error, bool) { 38 req := resp.Request 39 env := req.Context().Value(environmentContextKey).(map[string]string) 40 if env["SYMFONY_TUNNEL"] != "" && env["SYMFONY_TUNNEL_ENV"] == "" { 41 p.logger.Warn().Msgf("Tunnel to %s open but environment variables not exposed", env["SYMFONY_TUNNEL_BRAND"]) 42 } 43 44 if req.Method != http.MethodGet || req.Header.Get("x-requested-with") != "XMLHttpRequest" { 45 return nil, false 46 } 47 48 if baseCT, _, _ := mime.ParseMediaType(resp.Header.Get("content-type")); baseCT != "text/html" { 49 return nil, false 50 } 51 52 var err error 53 if resp.Body, err = p.tweakToolbar(resp.Body, env); err != nil { 54 return err, true 55 } 56 57 // we changed the body content, so we drop the incoming Content-Length, Go 58 // will recompute it automatically anyway 59 resp.Header.Del("content-length") 60 61 return nil, true 62 } 63 64 func (p *Server) tweakToolbar(body io.ReadCloser, env map[string]string) (io.ReadCloser, error) { 65 // CGI adds a \n at the start of the toolbar code 66 bn := bytes.Repeat([]byte{' '}, 1) 67 n, err := body.Read(bn) 68 // if body is empty, return immediately 69 if n == 0 && err == io.EOF { 70 return io.NopCloser(bytes.NewReader([]byte{})), nil 71 } 72 if n == len(bn) && err != nil { 73 return nil, errors.WithStack(err) 74 } 75 if bn[0] != '\n' && bn[0] != '<' { 76 return struct { 77 io.Reader 78 io.Closer 79 }{ 80 io.MultiReader(bytes.NewReader(bn), body), 81 body, 82 }, nil 83 } 84 85 toolbarHint := []byte("<!-- START of Symfony Web Debug Toolbar -->") 86 if bn[0] == '<' { 87 toolbarHint = toolbarHint[1:] 88 } 89 start := bytes.Repeat([]byte{' '}, len(toolbarHint)) 90 n, err = body.Read(start) 91 if n == len(start) && err != nil { 92 return nil, errors.WithStack(err) 93 } 94 if n != len(toolbarHint) || !bytes.Equal(start, toolbarHint) { 95 return struct { 96 io.Reader 97 io.Closer 98 }{ 99 io.MultiReader(bytes.NewReader(bn), bytes.NewReader(start), body), 100 body, 101 }, nil 102 } 103 104 logoBg := "sf-toolbar-status-normal" 105 tunnel := `<span class="sf-toolbar-status sf-toolbar-status-red">Down</span>` 106 docker := `<span class="sf-toolbar-status sf-toolbar-status-red">Down</span>` 107 envVars := `<span class="sf-toolbar-status sf-toolbar-status-red">None</span>` 108 if env["SYMFONY_TUNNEL"] != "" { 109 tunnel = fmt.Sprintf(`<span class="sf-toolbar-status sf-toolbar-status-green">Up (%s)</span>`, env["SYMFONY_TUNNEL"]) 110 if env["SYMFONY_TUNNEL_ENV"] != "" { 111 envVars = fmt.Sprintf(`<span class="sf-toolbar-status sf-toolbar-status-green">from %s</span>`, env["SYMFONY_TUNNEL_BRAND"]) 112 logoBg = "sf-toolbar-status-green" 113 } else { 114 logoBg = "sf-toolbar-status-yellow" 115 } 116 } 117 118 if env["SYMFONY_DOCKER_ENV"] == "1" { 119 docker = `<span class="sf-toolbar-status sf-toolbar-status-green">Up</span>` 120 logoBg = "sf-toolbar-status-green" 121 if env["SYMFONY_TUNNEL_ENV"] == "" { 122 envVars = `<span class="sf-toolbar-status sf-toolbar-status-green">from Docker</span>` 123 } 124 } 125 126 webmail := `<b>Webmail</b> <span class="sf-toolbar-status sf-toolbar-status-red">Down</span>` 127 rabbitmqui := `<b>RabbitMQ UI</b> <span class="sf-toolbar-status sf-toolbar-status-red">Down</span>` 128 blackfire := `<span class="sf-toolbar-status sf-toolbar-status-red">Down</span>` 129 extraLinks := `` 130 if env, err := envs.NewLocal(p.projectDir, terminal.IsDebug()); err == nil { 131 if url, exists := env.FindServiceUrl("mailer"); exists { 132 webmail = fmt.Sprintf(`<b><a class="sf-cli-webmail" href="%s" rel="noopener" target="_blank">Webmail</a></b> <span class="sf-toolbar-status sf-toolbar-status-green">Up</span></a>`, url) 133 } 134 if url, exists := env.FindServiceUrl("amqp"); exists { 135 rabbitmqui = fmt.Sprintf(`<b><a class="sf-cli-rabbitmq" href="%s" rel="noopener" target="_blank">RabbitMQ UI</a></b> <span class="sf-toolbar-status sf-toolbar-status-green">Up</span></a>`, url) 136 } 137 if prefix := env.FindRelationshipPrefix("blackfire", "tcp"); prefix != "" { 138 blackfire = `<span class="sf-toolbar-status sf-toolbar-status-green">Up</span>` 139 } 140 for _, service := range env.FindHttpServices() { 141 if service == "mailer-web" || service == "amqp" { 142 continue 143 } 144 145 if url, exists := env.FindServiceUrl(service); exists { 146 extraLinks += fmt.Sprintf(`<div class="sf-toolbar-info-piece"><b><a class="sf-cli-service-link" href="%s" rel="noopener" target="_blank">%s</a></b></div>`, url, cases.Title(language.Und).String(service)) 147 } 148 } 149 150 if len(extraLinks) > 0 { 151 extraLinks = `</div><div class="sf-toolbar-info-group">` + extraLinks 152 } 153 } 154 155 b, err := io.ReadAll(body) 156 if err != nil { 157 return body, errors.WithStack(err) 158 } 159 content := []byte(` 160 <div class="sf-cli sf-toolbar-block sf-toolbar-block-sf-cli ` + logoBg + ` sf-toolbar-block-right"> 161 <div class="sf-toolbar-icon"> 162 <span class="sf-toolbar-label"> 163 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24" height="24" viewBox="0 0 24 24" enable-background="new 0 0 24 24" version="1.1"> 164 <path fill="#fff" fill-rule="evenodd" d="M 24 2.398438 C 24 1.074219 22.925781 0 21.601562 0 L 2.398438 0 C 1.074219 0 0 1.074219 0 2.398438 L 0 21.601562 C 0 22.925781 1.074219 24 2.398438 24 L 21.601562 24 C 22.925781 24 24 22.925781 24 21.601562 Z M 24 2.398438"/> 165 <path d="M 18.078125 3.109375 C 16.742188 3.15625 15.578125 3.894531 14.710938 4.910156 C 13.75 6.027344 13.109375 7.351562 12.648438 8.703125 C 11.824219 8.027344 11.191406 7.152344 9.867188 6.773438 C 8.847656 6.480469 7.773438 6.601562 6.785156 7.335938 C 6.320312 7.683594 5.996094 8.210938 5.84375 8.710938 C 5.449219 9.996094 6.261719 11.144531 6.628906 11.558594 L 7.4375 12.421875 C 7.605469 12.59375 8.007812 13.035156 7.808594 13.667969 C 7.597656 14.359375 6.765625 14.804688 5.914062 14.542969 C 5.53125 14.425781 4.984375 14.144531 5.105469 13.742188 C 5.15625 13.578125 5.273438 13.457031 5.335938 13.316406 C 5.394531 13.195312 5.421875 13.105469 5.441406 13.050781 C 5.597656 12.542969 5.382812 11.878906 4.835938 11.710938 C 4.328125 11.554688 3.808594 11.679688 3.605469 12.332031 C 3.378906 13.078125 3.734375 14.425781 5.640625 15.015625 C 7.875 15.703125 9.765625 14.484375 10.035156 12.898438 C 10.203125 11.90625 9.753906 11.167969 8.933594 10.21875 L 8.261719 9.476562 C 7.855469 9.070312 7.71875 8.378906 8.136719 7.847656 C 8.492188 7.402344 8.996094 7.210938 9.824219 7.433594 C 11.03125 7.761719 11.566406 8.597656 12.464844 9.273438 C 12.09375 10.492188 11.851562 11.710938 11.632812 12.804688 L 11.5 13.621094 C 10.855469 16.984375 10.367188 18.832031 9.09375 19.890625 C 8.839844 20.074219 8.472656 20.347656 7.917969 20.367188 C 7.628906 20.375 7.535156 20.175781 7.53125 20.089844 C 7.527344 19.886719 7.695312 19.792969 7.808594 19.703125 C 7.980469 19.609375 8.238281 19.457031 8.21875 18.960938 C 8.203125 18.378906 7.71875 17.875 7.023438 17.898438 C 6.5 17.917969 5.703125 18.40625 5.734375 19.308594 C 5.765625 20.238281 6.628906 20.933594 7.9375 20.890625 C 8.636719 20.867188 10.195312 20.582031 11.730469 18.753906 C 13.519531 16.660156 14.019531 14.261719 14.394531 12.503906 L 14.816406 10.183594 C 15.050781 10.210938 15.300781 10.230469 15.570312 10.238281 C 17.796875 10.285156 18.910156 9.132812 18.929688 8.292969 C 18.941406 7.785156 18.597656 7.285156 18.113281 7.296875 C 17.769531 7.304688 17.335938 7.535156 17.230469 8.011719 C 17.128906 8.480469 17.941406 8.902344 17.308594 9.3125 C 16.855469 9.605469 16.050781 9.808594 14.914062 9.644531 L 15.121094 8.5 C 15.542969 6.335938 16.0625 3.671875 18.035156 3.609375 C 18.179688 3.601562 18.703125 3.613281 18.71875 3.960938 C 18.722656 4.078125 18.691406 4.109375 18.554688 4.375 C 18.417969 4.582031 18.363281 4.757812 18.371094 4.960938 C 18.390625 5.511719 18.8125 5.875 19.417969 5.855469 C 20.234375 5.828125 20.46875 5.035156 20.453125 4.628906 C 20.421875 3.671875 19.410156 3.066406 18.078125 3.109375 Z M 18.078125 3.109375"/> 166 </svg> 167 </span> 168 <span class="sf-toolbar-value">Server</span> 169 </div> 170 <div class="sf-toolbar-info"> 171 <div class="sf-toolbar-info-group"> 172 <div class="sf-toolbar-info-piece"> 173 <b>Symfony CLI</b>` + p.appVersion + ` 174 </div> 175 <div class="sf-toolbar-info-piece"> 176 <b>PHP</b>` + p.Version.ServerTypeName() + ` ` + p.Version.Version + ` 177 </div> 178 <div class="sf-toolbar-info-piece"> 179 <b>Tunnel</b>` + tunnel + ` 180 </div> 181 <div class="sf-toolbar-info-piece"> 182 <b>Docker Compose</b>` + docker + ` 183 </div> 184 <div class="sf-toolbar-info-piece"> 185 <b>Env Vars</b>` + envVars + ` 186 </div> 187 <div class="sf-toolbar-info-piece">` + rabbitmqui + `</div> 188 <div class="sf-toolbar-info-piece">` + webmail + `</div> 189 <div class="sf-toolbar-info-piece"> 190 <b><a class="sf-cli-blackfire" href="https://blackfire.io/" rel="noopener" target="_blank">Blackfire.io Agent</a></b>` + blackfire + `</div> 191 ` + extraLinks + ` 192 </div> 193 </div> 194 <div></div> 195 </div> 196 $1`) 197 198 re := regexp.MustCompile(`(<(?:a|button)[^"]+?class="hide-button")`) 199 b = re.ReplaceAll(b, content) 200 201 return struct { 202 io.Reader 203 io.Closer 204 }{ 205 io.MultiReader(bytes.NewReader(bn), bytes.NewReader(start), bytes.NewReader(b)), 206 body, 207 }, nil 208 }