gl-website-deployer/admin/phpMyAdmin/libraries/classes/ErrorReport.php
2024-11-19 08:02:04 +01:00

299 lines
9.0 KiB
PHP

<?php
declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Utils\HttpRequest;
use function count;
use function http_build_query;
use function is_array;
use function json_encode;
use function mb_strlen;
use function mb_substr;
use function parse_str;
use function parse_url;
use function preg_match;
use function str_replace;
use const E_USER_WARNING;
use const PHP_VERSION;
/**
* Error reporting functions used to generate and submit error reports
*/
class ErrorReport
{
/**
* The URL where to submit reports to
*
* @var string
*/
private $submissionUrl = 'https://reports.phpmyadmin.net/incidents/create';
/** @var HttpRequest */
private $httpRequest;
/** @var Relation */
private $relation;
/** @var Template */
public $template;
/** @var Config */
private $config;
/**
* @param HttpRequest $httpRequest HttpRequest instance
* @param Relation $relation Relation instance
* @param Template $template Template instance
*/
public function __construct(HttpRequest $httpRequest, Relation $relation, Template $template, Config $config)
{
$this->httpRequest = $httpRequest;
$this->relation = $relation;
$this->template = $template;
$this->config = $config;
}
/**
* Set the URL where to submit reports to
*
* @param string $submissionUrl Submission URL
*/
public function setSubmissionUrl(string $submissionUrl): void
{
$this->submissionUrl = $submissionUrl;
}
/**
* Returns the error report data collected from the current configuration or
* from the request parameters sent by the error reporting js code.
*
* @param string $exceptionType whether exception is 'js' or 'php'
*
* @return array error report if success, Empty Array otherwise
*/
public function getData(string $exceptionType = 'js'): array
{
$relationParameters = $this->relation->getRelationParameters();
// common params for both, php & js exceptions
$report = [
'pma_version' => Version::VERSION,
'browser_name' => $this->config->get('PMA_USR_BROWSER_AGENT'),
'browser_version' => $this->config->get('PMA_USR_BROWSER_VER'),
'user_os' => $this->config->get('PMA_USR_OS'),
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? null,
'user_agent_string' => $_SERVER['HTTP_USER_AGENT'],
'locale' => $this->config->getCookie('pma_lang'),
'configuration_storage' => $relationParameters->db === null ? 'disabled' : 'enabled',
'php_version' => PHP_VERSION,
];
if ($exceptionType === 'js') {
if (empty($_POST['exception'])) {
return [];
}
$exception = $_POST['exception'];
if (isset($exception['stack'])) {
$exception['stack'] = $this->translateStacktrace($exception['stack']);
}
if (isset($exception['url'])) {
[$uri, $scriptName] = $this->sanitizeUrl($exception['url']);
$exception['uri'] = $uri;
$report['script_name'] = $scriptName;
unset($exception['url']);
} elseif (isset($_POST['url'])) {
[$uri, $scriptName] = $this->sanitizeUrl($_POST['url']);
$exception['uri'] = $uri;
$report['script_name'] = $scriptName;
unset($_POST['url']);
} else {
$report['script_name'] = null;
}
$report['exception_type'] = 'js';
$report['exception'] = $exception;
if (! empty($_POST['description'])) {
$report['steps'] = $_POST['description'];
}
} elseif ($exceptionType === 'php') {
$errors = [];
// create php error report
$i = 0;
if (! isset($_SESSION['prev_errors']) || $_SESSION['prev_errors'] == '') {
return [];
}
foreach ($_SESSION['prev_errors'] as $errorObj) {
/** @var Error $errorObj */
if (! $errorObj->getLine() || ! $errorObj->getType() || $errorObj->getNumber() == E_USER_WARNING) {
continue;
}
$errors[$i++] = [
'lineNum' => $errorObj->getLine(),
'file' => $errorObj->getFile(),
'type' => $errorObj->getType(),
'msg' => $errorObj->getOnlyMessage(),
'stackTrace' => $errorObj->getBacktrace(5),
'stackhash' => $errorObj->getHash(),
];
}
// if there were no 'actual' errors to be submitted.
if ($i == 0) {
return []; // then return empty array
}
$report['exception_type'] = 'php';
$report['errors'] = $errors;
} else {
return [];
}
return $report;
}
/**
* Sanitize a url to remove the identifiable host name and extract the
* current script name from the url fragment
*
* It returns two things in an array. The first is the uri without the
* hostname and identifying query params. The second is the name of the
* php script in the url
*
* @param string $url the url to sanitize
*
* @return array the uri and script name
*/
private function sanitizeUrl(string $url): array
{
$components = parse_url($url);
if (! is_array($components)) {
$components = [];
}
if (isset($components['fragment']) && preg_match('<PMAURL-\d+:>', $components['fragment'], $matches)) {
$uri = str_replace($matches[0], '', $components['fragment']);
$url = 'https://example.com/' . $uri;
$components = parse_url($url);
if (! is_array($components)) {
$components = [];
}
}
// get script name
preg_match('<([a-zA-Z\-_\d\.]*\.php|js\/[a-zA-Z\-_\d\/\.]*\.js)$>', $components['path'] ?? '', $matches);
if (count($matches) < 2) {
$scriptName = 'index.php';
} else {
$scriptName = $matches[1];
}
// remove deployment specific details to make uri more generic
if (isset($components['query'])) {
parse_str($components['query'], $queryArray);
unset($queryArray['db'], $queryArray['table'], $queryArray['token'], $queryArray['server']);
unset($queryArray['eq']);
$query = http_build_query($queryArray);
} else {
$query = '';
}
$uri = $scriptName . '?' . $query;
return [
$uri,
$scriptName,
];
}
/**
* Sends report data to the error reporting server
*
* @param array $report the report info to be sent
*
* @return string|bool|null the reply of the server
*/
public function send(array $report)
{
return $this->httpRequest->create(
$this->submissionUrl,
'POST',
false,
json_encode($report),
'Content-Type: application/json'
);
}
/**
* Translates the cumulative line numbers in the stack trace as well as sanitize
* urls and trim long lines in the context
*
* @param array $stack the stack trace
*
* @return array the modified stack trace
*/
private function translateStacktrace(array $stack): array
{
foreach ($stack as &$level) {
foreach ($level['context'] as &$line) {
if (mb_strlen($line) <= 80) {
continue;
}
$line = mb_substr($line, 0, 75) . '//...';
}
[$uri, $scriptName] = $this->sanitizeUrl($level['url']);
$level['uri'] = $uri;
$level['scriptname'] = $scriptName;
unset($level['url']);
}
unset($level);
return $stack;
}
/**
* Generates the error report form to collect user description and preview the
* report before being sent
*
* @return string the form
*/
public function getForm(): string
{
$reportData = $this->getData();
$datas = [
'report_data' => $reportData,
'hidden_inputs' => Url::getHiddenInputs(),
'hidden_fields' => null,
'allowed_to_send_error_reports' => $this->config->get('SendErrorReports') !== 'never',
];
if (! empty($reportData)) {
$datas['hidden_fields'] = Url::getHiddenFields($reportData, '', true);
}
return $this->template->render('error/report_form', $datas);
}
public function getEmptyModal(): string
{
return $this->template->render('error/report_modal', [
'allowed_to_send_error_reports' => $this->config->get('SendErrorReports') !== 'never',
]);
}
}