diff --git a/app/Http/Controllers/BuildController.php b/app/Http/Controllers/BuildController.php index c81d72daf2..7e84f5b6a5 100644 --- a/app/Http/Controllers/BuildController.php +++ b/app/Http/Controllers/BuildController.php @@ -186,11 +186,6 @@ public function buildOverview(): View|RedirectResponse ]); } - public function buildProperties(): View - { - return view('build.properties'); - } - public function viewFiles(): View|RedirectResponse { if (!isset($_GET['buildid'])) { diff --git a/app/Http/Controllers/BuildPropertiesController.php b/app/Http/Controllers/BuildPropertiesController.php new file mode 100644 index 0000000000..8d93d6efc3 --- /dev/null +++ b/app/Http/Controllers/BuildPropertiesController.php @@ -0,0 +1,300 @@ +json($this->get_defects_for_builds()); + } + + if (!isset($_GET['project'])) { + abort(400, 'Valid project required'); + } + $this->setProjectByName($_GET['project']); + + // Begin our JSON response. + $response = begin_JSON_response(); + $response['title'] = "{$this->project->Name} - Build Properties"; + $response['showcalendar'] = 0; + $response['nightlytime'] = $this->project->NightlyTime; + + // Figure out our time range. + $date = null; + $beginning_timestamp = null; + $end_timestamp = null; + if (isset($_GET['begin']) && isset($_GET['end'])) { + $beginning_date = $_GET['begin']; + $end_date = $_GET['end']; + list($unused, $beginning_timestamp) = get_dates($beginning_date, $this->project->NightlyTime); + list($unused, $end_timestamp) = get_dates($end_date, $this->project->NightlyTime); + $datetime = new DateTime(); + $datetime->setTimestamp($end_timestamp); + $datetime->add(new DateInterval('P1D')); + $end_timestamp = $datetime->getTimestamp(); + $response['begin'] = $beginning_date; + $response['end'] = $end_date; + } elseif (isset($_GET['date'])) { + // Otherwise use the provided date (if any). + $date = $_GET['date']; + } else { + // Default to the current date. + $date = date(FMT_DATE); + } + if (is_null($beginning_timestamp)) { + list($unused, $beginning_timestamp) = get_dates($date, $this->project->NightlyTime); + $datetime = new DateTime(); + $datetime->setTimestamp($beginning_timestamp); + $datetime->add(new DateInterval('P1D')); + $end_timestamp = $datetime->getTimestamp(); + } + $begin_date = date(FMT_DATETIME, $beginning_timestamp); + $end_date = date(FMT_DATETIME, $end_timestamp); + + get_dashboard_JSON($this->project->Name, date(FMT_DATE, $end_timestamp), $response); + + // Hide traditional Previous/Current/Next links. + $response['hidenav'] = true; + + // List of possible types of defects to track. + $defect_types = [ + [ + 'name' => 'builderrors', + 'prettyname' => 'Errors', + 'selected' => false + ], + [ + 'name' => 'buildwarnings', + 'prettyname' => 'Warnings', + 'selected' => false + ], + [ + 'name' => 'testfailed', + 'prettyname' => 'Test Failures', + 'selected' => false + ] + ]; + + // Mark specified types of defects as selected. + if (isset($_GET['defects'])) { + $selected_defect_types = explode(',', $_GET['defects']); + foreach ($selected_defect_types as $selected_type) { + foreach ($defect_types as &$type) { + if ($type['name'] === $selected_type) { + $type['selected'] = true; + break; + } + } + unset($type); + } + } else { + // Use the full list if none was specified. + foreach ($defect_types as &$type) { + $type['selected'] = true; + } + unset($type); + } + $response['defecttypes'] = $defect_types; + + $defect_types = array_filter($defect_types, function ($defect_type) { + return $defect_type['selected']; + }); + + // Construct an SQL SELECT clause for the requested types of defects. + $defect_keys = []; + foreach ($defect_types as $type) { + $defect_keys[] = "b.{$type['name']}"; + } + $defect_selection = implode(', ', $defect_keys); + + // Get properties and error info for selected builds. + $pdo = Database::getInstance()->getPdo(); + $stmt = $pdo->prepare( + "SELECT b.id, b.name, $defect_selection, bp.properties + FROM build b + JOIN buildproperties bp ON (bp.buildid = b.id) + WHERE b.projectid = :projectid AND b.parentid IN (0, -1) + AND b.starttime < :end AND b.starttime >= :begin"); + $stmt->bindParam(':projectid', $this->project->Id); + $stmt->bindParam(':begin', $begin_date); + $stmt->bindParam(':end', $end_date); + pdo_execute($stmt); + + $builds_response = []; + $all_properties = []; + while ($row = $stmt->fetch()) { + $build_response = []; + $buildid = $row['id']; + $build_response['id'] = $buildid; + foreach ($defect_types as $defect_type) { + $key = $defect_type['name']; + $build_response[$key] = $row[$key]; + } + $properties = json_decode($row['properties'], true); + $build_response['properties'] = $properties; + $builds_response[] = $build_response; + + // Check for properties we haven't encountered yet. + $new_property_keys = array_diff(array_keys($properties), array_keys($all_properties)); + foreach ($new_property_keys as $key) { + // Determine what type of property this is. + $value = $properties[$key]; + if (is_array($value)) { + $type = 'array'; + } elseif (is_bool($value)) { + $type = 'bool'; + } elseif (is_numeric($value) && !str_contains($value, 'e')) { + $type = 'number'; + } else { + $type = 'string'; + } + + // Add it to our list. + $all_properties[$key] = ['type' => $type]; + } + } + $response['builds'] = $builds_response; + $response['properties'] = $all_properties; + + // Timeline chart needs to know what defects we care about + // and what page we're coming from. + Session::put('defecttypes', $defect_types); + + $response['filterdata']['pageId'] = 'buildProperties.php'; + + $pageTimer->end($response); + return response()->json(cast_data_for_JSON($response)); + } + + private function get_defects_for_builds() + { + if (!array_key_exists('buildid', $_GET)) { + abort(400, 'Missing parameter: buildid'); + } + if (!array_key_exists('defect', $_GET)) { + abort(400, 'Missing parameter: defect'); + } + + if (!is_array($_GET['buildid']) || count($_GET['buildid']) < 1) { + abort(400, "No builds specified"); + } + if (!is_array($_GET['defect']) || count($_GET['defect']) < 1) { + abort(400, "No defects specified"); + } + + // A bit of a hack to ensure that we are able to access each specified buildid. + // This loop will generate many queries and is generally quite inefficient, but should only + // be operating on small numbers of buildids, so we shouldn't see much of a performance impact. + // Future optimizations can be made at the cost of lower code cleanliness in the future if necessary. + foreach ($_GET['buildid'] as $buildid) { + $this->setBuildById((int) $buildid); + } + + $pdo = Database::getInstance()->getPdo(); + $placeholder_str = Database::getInstance()->createPreparedArray(count($_GET['buildid'])); + + $defects_response = []; + foreach ($_GET['defect'] as $defect) { + $valid_defect = false; + $sql = ''; + $sql2 = null; + + $error_defect = false; + $type = ''; + $prettyname = ''; + if ($defect === 'builderrors') { + $error_defect = true; + $prettyname = 'Error'; + $type = 0; + } elseif ($defect === 'buildwarnings') { + $error_defect = true; + $prettyname = 'Warning'; + $type = 1; + } + + if ($error_defect) { + $valid_defect = true; + // Query builderror table. + $sql = + "SELECT buildid, text AS descr + FROM builderror + WHERE type = $type AND + buildid IN $placeholder_str"; + // Query buildfailure table. + $sql2 = + "SELECT bf.buildid, bfd.stderror AS descr + FROM buildfailure bf + JOIN buildfailuredetails bfd ON bf.detailsid = bfd.id + WHERE bfd.type = $type AND + bf.buildid IN $placeholder_str"; + } elseif ($defect === 'testfailed') { + $valid_defect = true; + $prettyname = 'Test Failure'; + $sql = + "SELECT t.name AS descr, b.id AS buildid + FROM test t + JOIN build2test b2t ON b2t.testid = t.id + JOIN build b ON b.id = b2t.buildid + WHERE b2t.status = 'failed' AND + b.id IN $placeholder_str"; + } + + if (!$valid_defect) { + continue; + } + + $stmt = $pdo->prepare($sql); + $stmt->execute($_GET['buildid']); + + if (!$this->gather_defects($stmt, $prettyname, $defects_response) && !is_null($sql2)) { + $stmt = $pdo->prepare($sql2); + $stmt->execute($_GET['buildid']); + $this->gather_defects($stmt, $prettyname, $defects_response); + } + } + + $response = []; + $response['defects'] = $defects_response; + return cast_data_for_JSON($response); + } + + private function gather_defects(PDOStatement $stmt, string $prettyname, array &$defects_response): bool + { + $results_found = false; + while ($row = $stmt->fetch()) { + $results_found = true; + $descr = $row['descr']; + $idx = array_search($descr, array_column($defects_response, 'descr'), true); + if ($idx === false) { + $defects_response[] = [ + 'descr' => $descr, + 'type' => $prettyname, + 'builds' => [] + ]; + $idx = count($defects_response) - 1; + } + $defects_response[$idx]['builds'][] = $row['buildid']; + } + return $results_found; + } +} diff --git a/app/cdash/public/api/v1/buildProperties.php b/app/cdash/public/api/v1/buildProperties.php deleted file mode 100644 index 09b8f6ddf3..0000000000 --- a/app/cdash/public/api/v1/buildProperties.php +++ /dev/null @@ -1,305 +0,0 @@ -getPdo(); - $placeholder_str = str_repeat('?,', count($_GET['buildid']) - 1). '?'; - - if (!function_exists('CDash\Api\v1\BuildProperties\gather_defects')) { - function gather_defects($stmt, $prettyname, &$defects_response) - { - $results_found = false; - while ($row = $stmt->fetch()) { - $results_found = true; - $descr = $row['descr']; - $idx = array_search($descr, array_column($defects_response, 'descr')); - if ($idx === false) { - $defects_response[] = [ - 'descr' => $descr, - 'type' => $prettyname, - 'builds' => [] - ]; - $idx = count($defects_response) - 1; - } - $defects_response[$idx]['builds'][] = $row['buildid']; - } - return $results_found; - } - } - - $defects_response = []; - foreach ($_GET['defect'] as $defect) { - $valid_defect = false; - $sql = ''; - $sql2 = null; - - $error_defect = false; - if ($defect == 'builderrors') { - $error_defect = true; - $prettyname = 'Error'; - $type = 0; - } elseif ($defect == 'buildwarnings') { - $error_defect = true; - $prettyname = 'Warning'; - $type = 1; - } - - if ($error_defect) { - $valid_defect = true; - // Query builderror table. - $sql = - "SELECT buildid, text AS descr - FROM builderror - WHERE type = $type AND - buildid IN ($placeholder_str)"; - // Query buildfailure table. - $sql2 = - "SELECT bf.buildid, bfd.stderror AS descr - FROM buildfailure bf - JOIN buildfailuredetails bfd ON bf.detailsid = bfd.id - WHERE bfd.type = $type AND - bf.buildid IN ($placeholder_str)"; - } elseif ($defect == 'testfailed') { - $valid_defect = true; - $prettyname = 'Test Failure'; - $sql = - "SELECT t.name AS descr, b.id AS buildid - FROM test t - JOIN build2test b2t ON b2t.testid = t.id - JOIN build b ON b.id = b2t.buildid - WHERE b2t.status = 'failed' AND - b.id IN ($placeholder_str)"; - } - - if (!$valid_defect) { - continue; - } - - $stmt = $pdo->prepare($sql); - $stmt->execute($_GET['buildid']); - - if (!gather_defects($stmt, $prettyname, $defects_response) && - !is_null($sql2)) { - $stmt = $pdo->prepare($sql2); - $stmt->execute($_GET['buildid']); - gather_defects($stmt, $prettyname, $defects_response); - } - } - - $response = []; - $response['defects'] = $defects_response; - echo json_encode(cast_data_for_JSON($response)); - } -} - -$pageTimer = new PageTimer(); - -if (array_key_exists('buildid', $_GET)) { - get_defects_for_builds(); - return; -} - -// Make sure the user has access to this project. -$Project = get_project_from_request(); -if (is_null($Project)) { - return; -} - -// Load project data. -$Project->Fill(); - -// Begin our JSON response. -$response = begin_JSON_response(); -$response['title'] = "$Project->Name : Build Properties"; -$response['showcalendar'] = 0; -$response['nightlytime'] = $Project->NightlyTime; - -// Figure out our time range. -$date = null; -$beginning_timestamp = null; -$end_timestamp = null; -if (isset($_GET['begin']) && isset($_GET['end'])) { - $beginning_date = $_GET['begin']; - $end_date = $_GET['end']; - list($unused, $beginning_timestamp) = - get_dates($beginning_date, $Project->NightlyTime); - list($unused, $end_timestamp) = - get_dates($end_date, $Project->NightlyTime); - $datetime = new DateTime(); - $datetime->setTimeStamp($end_timestamp); - $datetime->add(new DateInterval('P1D')); - $end_timestamp = $datetime->getTimestamp(); - $response['begin'] = $beginning_date; - $response['end'] = $end_date; -} elseif (isset($_GET['date'])) { - // Otherwise use the provided date (if any). - $date = $_GET['date']; -} else { - // Default to the current date. - $date = date(FMT_DATE); -} -if (is_null($beginning_timestamp)) { - list($unused, $beginning_timestamp) = - get_dates($date, $Project->NightlyTime); - $datetime = new DateTime(); - $datetime->setTimeStamp($beginning_timestamp); - $datetime->add(new DateInterval('P1D')); - $end_timestamp = $datetime->getTimestamp(); -} -$begin_date = date(FMT_DATETIME, $beginning_timestamp); -$end_date = date(FMT_DATETIME, $end_timestamp); - -get_dashboard_JSON($Project->Name, date(FMT_DATE, $end_timestamp), $response); - -// Hide traditional Previous/Current/Next links. -$response['hidenav'] = true; - -// List of possible types of defects to track. -$defect_types = [ - [ - 'name' => 'builderrors', - 'prettyname' => 'Errors', - 'selected' => false - ], - [ - 'name' => 'buildwarnings', - 'prettyname' => 'Warnings', - 'selected' => false - ], - [ - 'name' => 'testfailed', - 'prettyname' => 'Test Failures', - 'selected' => false - ] -]; - -// Mark specified types of defects as selected. -if (isset($_GET['defects'])) { - $selected_defect_types = explode(',', $_GET['defects']); - foreach ($selected_defect_types as $selected_type) { - foreach ($defect_types as &$type) { - if ($type['name'] === $selected_type) { - $type['selected'] = true; - break; - } - } - unset($type); - } -} else { - // Use the full list if none was specified. - foreach ($defect_types as &$type) { - $type['selected'] = true; - } - unset($type); -} -$response['defecttypes'] = $defect_types; - -$defect_types = array_filter($defect_types, function ($defect_type) { - return $defect_type['selected']; -}); - -// Construct an SQL SELECT clause for the requested types of defects. -$defect_keys = []; -foreach ($defect_types as $type) { - $defect_keys[] = "b.{$type['name']}"; -} -$defect_selection = implode(', ', $defect_keys); - -// Get properties and error info for selected builds. -$pdo = Database::getInstance()->getPdo(); -$stmt = $pdo->prepare( - "SELECT b.id, b.name, $defect_selection, bp.properties - FROM build b - JOIN buildproperties bp ON (bp.buildid = b.id) - WHERE b.projectid = :projectid AND b.parentid IN (0, -1) - AND b.starttime < :end AND b.starttime >= :begin"); -$stmt->bindParam(':projectid', $Project->Id); -$stmt->bindParam(':begin', $begin_date); -$stmt->bindParam(':end', $end_date); -pdo_execute($stmt); - -$builds_response = []; -$all_properties = []; -while ($row = $stmt->fetch()) { - $build_response = []; - $buildid = $row['id']; - $build_response['id'] = $buildid; - foreach ($defect_types as $defect_type) { - $key = $defect_type['name']; - $build_response[$key] = $row[$key]; - } - $properties = json_decode($row['properties'], true); - $build_response['properties'] = $properties; - $builds_response[] = $build_response; - - // Check for properties we haven't encountered yet. - $new_property_keys = array_diff(array_keys($properties), array_keys($all_properties)); - foreach ($new_property_keys as $key) { - // Determine what type of property this is. - $value = $properties[$key]; - if (is_array($value)) { - $type = 'array'; - } elseif (is_bool($value)) { - $type = 'bool'; - } elseif (is_numeric($value) && strpos($value, 'e') === false) { - $type = 'number'; - } else { - $type = 'string'; - } - - // Add it to our list. - $all_properties[$key] = ['type' => $type]; - } -} -$response['builds'] = $builds_response; -$response['properties'] = $all_properties; - -// Timeline chart needs to know what defects we care about -// and what page we're coming from. -Session::put('defecttypes', $defect_types); - -$response['filterdata']['pageId'] = 'buildProperties.php'; - -$pageTimer->end($response); -echo json_encode(cast_data_for_JSON($response)); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6b0f7dfd31..4064b11c4b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -373,6 +373,42 @@ parameters: count: 1 path: app/Http/Controllers/BuildController.php + - + message: """ + #^Call to deprecated function pdo_execute\\(\\)\\: + v2\\.5\\.0 01/22/2018$# + """ + count: 1 + path: app/Http/Controllers/BuildPropertiesController.php + + - + message: """ + #^Call to deprecated method getPdo\\(\\) of class CDash\\\\Database\\: + 04/22/2023 Use Laravel query builder or Eloquent instead$# + """ + count: 2 + path: app/Http/Controllers/BuildPropertiesController.php + + - + message: "#^Empty array passed to foreach\\.$#" + count: 2 + path: app/Http/Controllers/BuildPropertiesController.php + + - + message: "#^Method App\\\\Http\\\\Controllers\\\\BuildPropertiesController\\:\\:gather_defects\\(\\) has parameter \\$defects_response with no value type specified in iterable type array\\.$#" + count: 1 + path: app/Http/Controllers/BuildPropertiesController.php + + - + message: "#^Method App\\\\Http\\\\Controllers\\\\BuildPropertiesController\\:\\:get_defects_for_builds\\(\\) has no return type specified\\.$#" + count: 1 + path: app/Http/Controllers/BuildPropertiesController.php + + - + message: "#^Method App\\\\Http\\\\Controllers\\\\BuildPropertiesController\\:\\:get_defects_for_builds\\(\\) throws checked exception PDOException but it's missing from the PHPDoc @throws tag\\.$#" + count: 2 + path: app/Http/Controllers/BuildPropertiesController.php + - message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#" count: 1 @@ -15476,87 +15512,6 @@ parameters: count: 1 path: app/cdash/public/api/v1/build.php - - - message: """ - #^Call to deprecated function pdo_execute\\(\\)\\: - v2\\.5\\.0 01/22/2018$# - """ - count: 1 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: """ - #^Call to deprecated method getPdo\\(\\) of class CDash\\\\Database\\: - 04/22/2023 Use Laravel query builder or Eloquent instead$# - """ - count: 2 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Call to function array_search\\(\\) requires parameter \\#3 to be set\\.$#" - count: 1 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Call to method DateTime\\:\\:setTimestamp\\(\\) with incorrect case\\: setTimeStamp$#" - count: 2 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Empty array passed to foreach\\.$#" - count: 2 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Function CDash\\\\Api\\\\v1\\\\BuildProperties\\\\gather_defects\\(\\) has no return type specified\\.$#" - count: 1 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Function CDash\\\\Api\\\\v1\\\\BuildProperties\\\\gather_defects\\(\\) has parameter \\$defects_response with no type specified\\.$#" - count: 1 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Function CDash\\\\Api\\\\v1\\\\BuildProperties\\\\gather_defects\\(\\) has parameter \\$prettyname with no type specified\\.$#" - count: 1 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Function CDash\\\\Api\\\\v1\\\\BuildProperties\\\\gather_defects\\(\\) has parameter \\$stmt with no type specified\\.$#" - count: 1 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Function CDash\\\\Api\\\\v1\\\\BuildProperties\\\\get_defects_for_builds\\(\\) has no return type specified\\.$#" - count: 1 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Function CDash\\\\Api\\\\v1\\\\BuildProperties\\\\get_defects_for_builds\\(\\) throws checked exception PDOException but it's missing from the PHPDoc @throws tag\\.$#" - count: 2 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Function gather_defects not found\\.$#" - count: 2 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Inner named functions are not supported by PHPStan\\. Consider refactoring to an anonymous function, class method, or a top\\-level\\-defined function\\. See issue \\#165 \\(https\\://github\\.com/phpstan/phpstan/issues/165\\) for more details\\.$#" - count: 1 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Variable \\$prettyname might not be defined\\.$#" - count: 2 - path: app/cdash/public/api/v1/buildProperties.php - - - - message: "#^Variable \\$type might not be defined\\.$#" - count: 2 - path: app/cdash/public/api/v1/buildProperties.php - - message: """ #^Call to deprecated method executePreparedSingleRow\\(\\) of class CDash\\\\Database\\: diff --git a/routes/api.php b/routes/api.php index 07ea9a7f6b..c2632acd86 100755 --- a/routes/api.php +++ b/routes/api.php @@ -35,6 +35,8 @@ Route::get('/v1/viewSubProjects.php', 'SubProjectController@apiViewSubProjects'); +Route::get('/v1/buildProperties.php', 'BuildPropertiesController@apiBuildProperties'); + Route::middleware(['auth'])->group(function () { Route::post('/authtokens/create', 'AuthTokenController@createToken'); Route::delete('/authtokens/delete/{token_hash}', 'AuthTokenController@deleteToken'); diff --git a/routes/web.php b/routes/web.php index 50040c9d46..36eae8282d 100755 --- a/routes/web.php +++ b/routes/web.php @@ -117,7 +117,7 @@ Route::get('/buildOverview.php', 'BuildController@buildOverview'); -Route::get('/buildProperties.php', 'BuildController@buildProperties'); +Route::get('/buildProperties.php', 'BuildPropertiesController@buildProperties'); Route::get('/viewSubProjectDependenciesGraph.php', 'SubProjectController@dependenciesGraph'); // TODO: (williamjallen) Replace this /ajax route with an equivalent /api route