perftools /
xhgui
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | |||
| 3 | namespace XHGui\Controller; |
||
| 4 | |||
| 5 | use Exception; |
||
| 6 | use Slim\Http\Request; |
||
| 7 | use Slim\Http\Response; |
||
| 8 | use Slim\Slim as App; |
||
| 9 | use XHGui\AbstractController; |
||
| 10 | use XHGui\Options\SearchOptions; |
||
| 11 | use XHGui\Searcher\SearcherInterface; |
||
| 12 | |||
| 13 | class RunController extends AbstractController |
||
| 14 | { |
||
| 15 | /** |
||
| 16 | * HTTP GET attribute name for comma separated filters |
||
| 17 | */ |
||
| 18 | private const FILTER_ARGUMENT_NAME = 'filter'; |
||
| 19 | |||
| 20 | /** |
||
| 21 | * @var SearcherInterface |
||
| 22 | */ |
||
| 23 | private $searcher; |
||
| 24 | |||
| 25 | public function __construct(App $app, SearcherInterface $searcher) |
||
| 26 | { |
||
| 27 | parent::__construct($app); |
||
| 28 | $this->searcher = $searcher; |
||
| 29 | } |
||
| 30 | |||
| 31 | public function index(Request $request, Response $response): void |
||
| 32 | { |
||
| 33 | // The list changes whenever new profiles are recorded. |
||
| 34 | // Generally avoid caching, but allow re-use in browser's bfcache |
||
| 35 | // and by cache proxies for concurrent requests. |
||
| 36 | // https://github.com/perftools/xhgui/issues/261 |
||
| 37 | $response->headers->set('Cache-Control', 'public, max-age=0'); |
||
| 38 | |||
| 39 | $search = []; |
||
| 40 | $keys = ['date_start', 'date_end', 'url']; |
||
| 41 | foreach ($keys as $key) { |
||
| 42 | if ($request->get($key)) { |
||
| 43 | $search[$key] = $request->get($key); |
||
| 44 | } |
||
| 45 | } |
||
| 46 | $sort = $request->get('sort'); |
||
| 47 | |||
| 48 | $result = $this->searcher->getAll(new SearchOptions([ |
||
| 49 | 'sort' => $sort, |
||
| 50 | 'page' => (int)$request->get('page', SearcherInterface::DEFAULT_PAGE), |
||
| 51 | 'direction' => $request->get('direction'), |
||
| 52 | 'perPage' => (int)$this->config('page.limit'), |
||
| 53 | 'conditions' => $search, |
||
| 54 | 'projection' => true, |
||
| 55 | ])); |
||
| 56 | |||
| 57 | $title = 'Recent runs'; |
||
| 58 | $titleMap = [ |
||
| 59 | 'wt' => 'Longest wall time', |
||
| 60 | 'cpu' => 'Most CPU time', |
||
| 61 | 'mu' => 'Highest memory use', |
||
| 62 | ]; |
||
| 63 | if (isset($titleMap[$sort])) { |
||
| 64 | $title = $titleMap[$sort]; |
||
| 65 | } |
||
| 66 | |||
| 67 | $paging = [ |
||
| 68 | 'total_pages' => $result['totalPages'], |
||
| 69 | 'page' => $result['page'], |
||
| 70 | 'sort' => $sort, |
||
| 71 | 'direction' => $result['direction'], |
||
| 72 | ]; |
||
| 73 | |||
| 74 | $this->render('runs/list.twig', [ |
||
| 75 | 'paging' => $paging, |
||
| 76 | 'base_url' => 'home', |
||
| 77 | 'runs' => $result['results'], |
||
| 78 | 'search' => $search, |
||
| 79 | 'has_search' => implode('', $search) !== '', |
||
| 80 | 'title' => $title, |
||
| 81 | ]); |
||
| 82 | } |
||
| 83 | |||
| 84 | public function view(Request $request, Response $response): void |
||
| 85 | { |
||
| 86 | // Permalink views to a specific run are meant to be public and immutable. |
||
| 87 | // But limit the cache to only a short period of time (enough to allow |
||
| 88 | // handling of abuse or other stampedes). This way we don't have to |
||
| 89 | // deal with any kind of purging system for when profiles are deleted, |
||
| 90 | // or for after XHGui itself is upgraded and static assets may be |
||
| 91 | // incompatible etc. |
||
| 92 | // https://github.com/perftools/xhgui/issues/261 |
||
| 93 | $response->headers->set('Cache-Control', 'public, max-age=60, must-revalidate'); |
||
| 94 | |||
| 95 | $detailCount = $this->config('detail.count'); |
||
| 96 | $result = $this->searcher->get($request->get('id')); |
||
| 97 | |||
| 98 | $result->calculateSelf(); |
||
| 99 | |||
| 100 | // Self wall time graph |
||
| 101 | $timeChart = $result->extractDimension('ewt', $detailCount); |
||
| 102 | |||
| 103 | // Memory Block |
||
| 104 | $memoryChart = $result->extractDimension('emu', $detailCount); |
||
| 105 | |||
| 106 | // Watched Functions Block |
||
| 107 | $watchedFunctions = []; |
||
| 108 | foreach ($this->searcher->getAllWatches() as $watch) { |
||
| 109 | $matches = $result->getWatched($watch['name']); |
||
| 110 | if ($matches) { |
||
| 111 | $watchedFunctions = array_merge($watchedFunctions, $matches); |
||
| 112 | } |
||
| 113 | } |
||
| 114 | |||
| 115 | if (false !== $request->get(self::FILTER_ARGUMENT_NAME, false)) { |
||
| 116 | $profile = $result->sort('ewt', $result->filter($result->getProfile(), $this->getFilters())); |
||
| 117 | } else { |
||
| 118 | $profile = $result->sort('ewt', $result->getProfile()); |
||
| 119 | } |
||
| 120 | |||
| 121 | $this->render('runs/view.twig', [ |
||
| 122 | 'profile' => $profile, |
||
| 123 | 'result' => $result, |
||
| 124 | 'wall_time' => $timeChart, |
||
| 125 | 'memory' => $memoryChart, |
||
| 126 | 'watches' => $watchedFunctions, |
||
| 127 | ]); |
||
| 128 | } |
||
| 129 | |||
| 130 | /** |
||
| 131 | * @return array |
||
| 132 | */ |
||
| 133 | protected function getFilters() |
||
| 134 | { |
||
| 135 | $request = $this->app->request(); |
||
| 136 | $filterString = $request->get(self::FILTER_ARGUMENT_NAME); |
||
| 137 | if (strlen($filterString) > 1 && $filterString !== 'true') { |
||
| 138 | $filters = array_map('trim', explode(',', $filterString)); |
||
| 139 | } else { |
||
| 140 | $filters = $this->config('run.view.filter.names'); |
||
| 141 | } |
||
| 142 | |||
| 143 | return $filters; |
||
| 144 | } |
||
| 145 | |||
| 146 | public function deleteForm(Request $request): void |
||
| 147 | { |
||
| 148 | $id = $request->get('id'); |
||
| 149 | if (!is_string($id) || !strlen($id)) { |
||
| 150 | throw new Exception('The "id" parameter is required.'); |
||
| 151 | } |
||
| 152 | |||
| 153 | // Get details |
||
| 154 | $result = $this->searcher->get($id); |
||
| 155 | |||
| 156 | $this->render('runs/delete-form.twig', [ |
||
| 157 | 'run_id' => $id, |
||
| 158 | 'result' => $result, |
||
| 159 | ]); |
||
| 160 | } |
||
| 161 | |||
| 162 | public function deleteSubmit(Request $request): void |
||
| 163 | { |
||
| 164 | $id = $request->post('id'); |
||
| 165 | // Don't call profilers->delete() unless $id is set, |
||
| 166 | // otherwise it will turn the null into a MongoId and return "Successful". |
||
| 167 | if (!is_string($id) || !strlen($id)) { |
||
| 168 | // Form checks this already, |
||
| 169 | // only reachable by handcrafted or malformed requests. |
||
| 170 | throw new Exception('The "id" parameter is required.'); |
||
| 171 | } |
||
| 172 | |||
| 173 | // Delete the profile run. |
||
| 174 | $this->searcher->delete($id); |
||
| 175 | |||
| 176 | $this->app->flash('success', 'Deleted profile ' . $id); |
||
| 177 | |||
| 178 | $this->app->redirect($this->app->urlFor('home')); |
||
| 179 | } |
||
| 180 | |||
| 181 | public function deleteAllForm(): void |
||
| 182 | { |
||
| 183 | $this->render('runs/delete-all-form.twig'); |
||
| 184 | } |
||
| 185 | |||
| 186 | public function deleteAllSubmit(): void |
||
| 187 | { |
||
| 188 | // Delete all profile runs. |
||
| 189 | $this->searcher->truncate(); |
||
| 190 | |||
| 191 | $this->app->flash('success', 'Deleted all profiles'); |
||
| 192 | |||
| 193 | $this->app->redirect($this->app->urlFor('home')); |
||
| 194 | } |
||
| 195 | |||
| 196 | public function url(Request $request): void |
||
| 197 | { |
||
| 198 | $pagination = [ |
||
| 199 | 'sort' => $request->get('sort'), |
||
| 200 | 'direction' => $request->get('direction'), |
||
| 201 | 'page' => $request->get('page'), |
||
| 202 | 'perPage' => $this->config('page.limit'), |
||
| 203 | ]; |
||
| 204 | |||
| 205 | $search = []; |
||
| 206 | $keys = ['date_start', 'date_end', 'limit', 'limit_custom']; |
||
| 207 | foreach ($keys as $key) { |
||
| 208 | $search[$key] = $request->get($key); |
||
| 209 | } |
||
| 210 | |||
| 211 | $runs = $this->searcher->getForUrl( |
||
| 212 | $request->get('url'), |
||
| 213 | $pagination, |
||
| 214 | $search |
||
| 215 | ); |
||
| 216 | |||
| 217 | if (isset($search['limit_custom']) && |
||
| 218 | strlen($search['limit_custom']) > 0 && |
||
| 219 | $search['limit_custom'][0] === 'P' |
||
| 220 | ) { |
||
| 221 | $search['limit'] = $search['limit_custom']; |
||
| 222 | } |
||
| 223 | |||
| 224 | $chartData = $this->searcher->getPercentileForUrl( |
||
| 225 | 90, |
||
| 226 | $request->get('url'), |
||
| 227 | $search |
||
| 228 | ); |
||
| 229 | |||
| 230 | $paging = [ |
||
| 231 | 'total_pages' => $runs['totalPages'], |
||
| 232 | 'sort' => $pagination['sort'], |
||
| 233 | 'page' => $runs['page'], |
||
| 234 | 'direction' => $runs['direction'], |
||
| 235 | ]; |
||
| 236 | |||
| 237 | $this->render('runs/url.twig', [ |
||
| 238 | 'paging' => $paging, |
||
| 239 | 'base_url' => 'url.view', |
||
| 240 | 'runs' => $runs['results'], |
||
| 241 | 'url' => $request->get('url'), |
||
| 242 | 'chart_data' => $chartData, |
||
| 243 | 'search' => array_merge($search, ['url' => $request->get('url')]), |
||
| 244 | ]); |
||
| 245 | } |
||
| 246 | |||
| 247 | public function compare(Request $request): void |
||
| 248 | { |
||
| 249 | $baseRun = $headRun = $candidates = $comparison = null; |
||
| 250 | $paging = []; |
||
| 251 | |||
| 252 | if ($request->get('base')) { |
||
| 253 | $baseRun = $this->searcher->get($request->get('base')); |
||
| 254 | } |
||
| 255 | |||
| 256 | if ($baseRun && !$request->get('head')) { |
||
| 257 | $pagination = [ |
||
| 258 | 'direction' => $request->get('direction'), |
||
| 259 | 'sort' => $request->get('sort'), |
||
| 260 | 'page' => $request->get('page'), |
||
| 261 | 'perPage' => $this->config('page.limit'), |
||
| 262 | ]; |
||
| 263 | $candidates = $this->searcher->getForUrl( |
||
| 264 | $baseRun->getMeta('simple_url'), |
||
| 265 | $pagination |
||
| 266 | ); |
||
| 267 | |||
| 268 | $paging = [ |
||
| 269 | 'total_pages' => $candidates['totalPages'], |
||
| 270 | 'sort' => $pagination['sort'], |
||
| 271 | 'page' => $candidates['page'], |
||
| 272 | 'direction' => $candidates['direction'], |
||
| 273 | ]; |
||
| 274 | } |
||
| 275 | |||
| 276 | if ($request->get('head')) { |
||
| 277 | $headRun = $this->searcher->get($request->get('head')); |
||
| 278 | } |
||
| 279 | |||
| 280 | if ($baseRun && $headRun) { |
||
| 281 | $comparison = $baseRun->compare($headRun); |
||
|
0 ignored issues
–
show
|
|||
| 282 | } |
||
| 283 | |||
| 284 | $this->render('runs/compare.twig', [ |
||
| 285 | 'base_url' => 'run.compare', |
||
| 286 | 'base_run' => $baseRun, |
||
| 287 | 'head_run' => $headRun, |
||
| 288 | 'candidates' => $candidates, |
||
| 289 | 'url_params' => $request->get(), |
||
| 290 | 'comparison' => $comparison, |
||
| 291 | 'paging' => $paging, |
||
| 292 | 'search' => [ |
||
| 293 | 'base' => $request->get('base'), |
||
| 294 | 'head' => $request->get('head'), |
||
| 295 | ], |
||
| 296 | ]); |
||
| 297 | } |
||
| 298 | |||
| 299 | public function symbol(Request $request): void |
||
| 300 | { |
||
| 301 | $id = $request->get('id'); |
||
| 302 | $symbol = $request->get('symbol'); |
||
| 303 | |||
| 304 | $profile = $this->searcher->get($id); |
||
| 305 | $profile->calculateSelf(); |
||
| 306 | [$parents, $current, $children] = $profile->getRelatives($symbol); |
||
|
0 ignored issues
–
show
|
|||
| 307 | |||
| 308 | $this->render('runs/symbol.twig', [ |
||
| 309 | 'symbol' => $symbol, |
||
| 310 | 'id' => $id, |
||
| 311 | 'main' => $profile->get('main()'), |
||
| 312 | 'parents' => $parents, |
||
| 313 | 'current' => $current, |
||
| 314 | 'children' => $children, |
||
| 315 | ]); |
||
| 316 | } |
||
| 317 | |||
| 318 | public function symbolShort(Request $request): void |
||
| 319 | { |
||
| 320 | $id = $request->get('id'); |
||
| 321 | $threshold = $request->get('threshold'); |
||
| 322 | $symbol = $request->get('symbol'); |
||
| 323 | $metric = $request->get('metric'); |
||
| 324 | |||
| 325 | $profile = $this->searcher->get($id); |
||
| 326 | $profile->calculateSelf(); |
||
| 327 | [$parents, $current, $children] = $profile->getRelatives($symbol, $metric, $threshold); |
||
|
0 ignored issues
–
show
|
|||
| 328 | |||
| 329 | $this->render('runs/symbol-short.twig', [ |
||
| 330 | 'symbol' => $symbol, |
||
| 331 | 'id' => $id, |
||
| 332 | 'main' => $profile->get('main()'), |
||
| 333 | 'parents' => $parents, |
||
| 334 | 'current' => $current, |
||
| 335 | 'children' => $children, |
||
| 336 | ]); |
||
| 337 | } |
||
| 338 | |||
| 339 | public function callgraph(Request $request): void |
||
| 340 | { |
||
| 341 | $profile = $this->searcher->get($request->get('id')); |
||
| 342 | |||
| 343 | $this->render('runs/callgraph.twig', [ |
||
| 344 | 'profile' => $profile, |
||
| 345 | ]); |
||
| 346 | } |
||
| 347 | |||
| 348 | public function callgraphData(Request $request, Response $response) |
||
| 349 | { |
||
| 350 | $profile = $this->searcher->get($request->get('id')); |
||
| 351 | $metric = $request->get('metric') ?: 'wt'; |
||
| 352 | $threshold = (float)$request->get('threshold') ?: 0.01; |
||
| 353 | $callgraph = $profile->getCallgraph($metric, $threshold); |
||
| 354 | |||
| 355 | $response['Content-Type'] = 'application/json'; |
||
| 356 | |||
| 357 | return $response->body(json_encode($callgraph)); |
||
| 358 | } |
||
| 359 | |||
| 360 | public function callgraphDataDot(Request $request, Response $response) |
||
| 361 | { |
||
| 362 | $profile = $this->searcher->get($request->get('id')); |
||
| 363 | $metric = $request->get('metric') ?: 'wt'; |
||
| 364 | $threshold = (float)$request->get('threshold') ?: 0.01; |
||
| 365 | $callgraph = $profile->getCallgraphNodes($metric, $threshold); |
||
|
0 ignored issues
–
show
The method
getCallgraphNodes() does not exist on XHGui\Profile. Did you maybe mean getCallgraph()?
This check marks calls to methods that do not seem to exist on an object. This is most likely the result of a method being renamed without all references to it being renamed likewise. Loading history...
|
|||
| 366 | |||
| 367 | $response['Content-Type'] = 'application/json'; |
||
| 368 | |||
| 369 | return $response->body(json_encode($callgraph)); |
||
| 370 | } |
||
| 371 | } |
||
| 372 |
It seems like the type of the argument is not accepted by the function/method which you are calling.
In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.
We suggest to add an explicit type cast like in the following example: