Git Komut Rehberi & Terminal Asistanı
Sıfırdan Yeni Bir Depo Başlatın:
Mevcut Bir Git Deposu veya Klasörü Gönderin:
Hataları Geri Alma, İptal & Temizlik:
Son commit'i geri al (yapılan değişiklikleri korur):
git reset --soft HEAD~1
Yapılan yerel değişiklikleri iptal et (çalışma ağacını temizler):
git restore .
Takip edilmeyen gereksiz dosyaları sil:
git clean -fd
Değişiklikleri geçici sakla ve geri getir (stash):
git stash git stash pop
Dal (Branch) ve Sürüm Etiketleri:
Yeni dal aç ve geçiş yap:
git checkout -b yeni-ozellik git push -u origin yeni-ozellik
Sürüm etiketi (tag) oluştur ve gönder:
git tag -a v1.0.0 -m "Sürüm 1.0.0" git push origin --tags
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c42ab714ff |
+3
-1
@@ -30,7 +30,9 @@
|
|||||||
"symfony/yaml": "^6.4|^7.0",
|
"symfony/yaml": "^6.4|^7.0",
|
||||||
"monolog/monolog": "^3.0",
|
"monolog/monolog": "^3.0",
|
||||||
"vlucas/phpdotenv": "^5.6",
|
"vlucas/phpdotenv": "^5.6",
|
||||||
"google-gemini-php/client": "^1.0"
|
"google-gemini-php/client": "^1.0",
|
||||||
|
"guzzlehttp/guzzle": "^7.8",
|
||||||
|
"masterminds/html5": "^2.8"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^10.5",
|
"phpunit/phpunit": "^10.5",
|
||||||
|
|||||||
+10
-19
@@ -1,19 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// 1. Setup Autoloading (Mocking Composer)
|
// 1. Setup Autoloading
|
||||||
require_once __DIR__ . '/../src/Scanner.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../src/Violation.php';
|
|
||||||
require_once __DIR__ . '/../src/SourceLocator.php';
|
|
||||||
require_once __DIR__ . '/../src/Reporter/HtmlReporter.php';
|
|
||||||
require_once __DIR__ . '/../src/Rule/RuleInterface.php';
|
|
||||||
require_once __DIR__ . '/../src/Rules/ImgAltText.php';
|
|
||||||
require_once __DIR__ . '/../src/Rules/FormLabel.php';
|
|
||||||
require_once __DIR__ . '/../src/Rules/HtmlHasLang.php';
|
|
||||||
require_once __DIR__ . '/../src/Rules/EmptyLink.php';
|
|
||||||
|
|
||||||
use YakNet\AccessibilityConsole\Scanner;
|
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||||
use YakNet\AccessibilityConsole\SourceLocator;
|
use YakNet\AccessibilityConsole\Source\SourceLocator;
|
||||||
use YakNet\AccessibilityConsole\Reporter\HtmlReporter;
|
use YakNet\AccessibilityConsole\Reporting\HtmlDashboardReporter;
|
||||||
use YakNet\AccessibilityConsole\Rules\ImgAltText;
|
use YakNet\AccessibilityConsole\Rules\ImgAltText;
|
||||||
use YakNet\AccessibilityConsole\Rules\FormLabel;
|
use YakNet\AccessibilityConsole\Rules\FormLabel;
|
||||||
|
|
||||||
@@ -48,19 +40,18 @@ $violations = $scanner->scan($html);
|
|||||||
|
|
||||||
if (!empty($violations)) {
|
if (!empty($violations)) {
|
||||||
// Locate Sources
|
// Locate Sources
|
||||||
// We point it to the 'views' directory so it knows where to look for the error source
|
|
||||||
$locator = new SourceLocator(__DIR__ . '/views');
|
$locator = new SourceLocator(__DIR__ . '/views');
|
||||||
|
|
||||||
foreach ($violations as $violation) {
|
foreach ($violations as $violation) {
|
||||||
$location = $locator->locate($violation->snippet);
|
$location = $locator->locate($violation->htmlSnippet);
|
||||||
if ($location) {
|
if ($location) {
|
||||||
$violation->setSourceLocation($location['file'], $location['line']);
|
$violation->location = $location;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render Report
|
// Render Report (Format as results mapped by page URL)
|
||||||
$reporter = new HtmlReporter();
|
$reporter = new HtmlDashboardReporter();
|
||||||
$reportHtml = $reporter->render($violations);
|
$reportHtml = $reporter->render(['http://localhost:8080/index.php' => $violations], 'demo/index.php');
|
||||||
|
|
||||||
// Inject into body
|
// Inject into body
|
||||||
$html = str_replace('</body>', $reportHtml . '</body>', $html);
|
$html = str_replace('</body>', $reportHtml . '</body>', $html);
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ class ScanCommand extends Command
|
|||||||
->addArgument('target', InputArgument::REQUIRED, 'The URL or file path to scan')
|
->addArgument('target', InputArgument::REQUIRED, 'The URL or file path to scan')
|
||||||
->addOption('ai', null, InputOption::VALUE_NONE, 'Enable AI-powered fix suggestions')
|
->addOption('ai', null, InputOption::VALUE_NONE, 'Enable AI-powered fix suggestions')
|
||||||
->addOption('report', null, InputOption::VALUE_REQUIRED, 'Save report to file (html or json)')
|
->addOption('report', null, InputOption::VALUE_REQUIRED, 'Save report to file (html or json)')
|
||||||
->addOption('project-path', null, InputOption::VALUE_REQUIRED, 'Base path for source mapping');
|
->addOption('project-path', null, InputOption::VALUE_REQUIRED, 'Base path for source mapping')
|
||||||
|
->addOption('crawl', null, InputOption::VALUE_NONE, 'Crawl internal links recursively')
|
||||||
|
->addOption('depth', null, InputOption::VALUE_REQUIRED, 'Maximum crawling depth', '3')
|
||||||
|
->addOption('max-pages', null, InputOption::VALUE_REQUIRED, 'Maximum number of pages to crawl', '20');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
@@ -46,73 +49,195 @@ class ScanCommand extends Command
|
|||||||
|
|
||||||
$io->title("Accessibility Console - Scanning: $target");
|
$io->title("Accessibility Console - Scanning: $target");
|
||||||
|
|
||||||
// 1. Fetch HTML
|
// 1. Setup Scanner
|
||||||
$html = @file_get_contents($target);
|
|
||||||
if ($html === false || $html === '') {
|
|
||||||
$io->error("Could not read target: $target");
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Setup Scanner
|
|
||||||
$scanner = new Scanner();
|
$scanner = new Scanner();
|
||||||
foreach (StandardRuleSet::all() as $rule) {
|
foreach (StandardRuleSet::all() as $rule) {
|
||||||
$scanner->addRule($rule);
|
$scanner->addRule($rule);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Scan
|
// 2. Fetch and Scan
|
||||||
$violations = $scanner->scan($html);
|
$isUrl = str_starts_with($target, 'http://') || str_starts_with($target, 'https://');
|
||||||
|
$crawl = (bool)$input->getOption('crawl') && $isUrl;
|
||||||
|
|
||||||
if (empty($violations)) {
|
/** @var array<string, \YakNet\AccessibilityConsole\Core\Violation[]> $results */
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
if ($crawl) {
|
||||||
|
$depthOption = $input->getOption('depth');
|
||||||
|
$depthLimit = is_numeric($depthOption) ? (int)$depthOption : 3;
|
||||||
|
$maxPagesOption = $input->getOption('max-pages');
|
||||||
|
$maxPages = is_numeric($maxPagesOption) ? (int)$maxPagesOption : 20;
|
||||||
|
|
||||||
|
$io->comment("Starting recursive crawl from: $target (Max Depth: $depthLimit, Max Pages: $maxPages)");
|
||||||
|
|
||||||
|
$parsedBase = parse_url($target);
|
||||||
|
$baseHost = $parsedBase['host'] ?? '';
|
||||||
|
|
||||||
|
$queue = [
|
||||||
|
['url' => $target, 'depth' => 1]
|
||||||
|
];
|
||||||
|
$visited = [];
|
||||||
|
|
||||||
|
$client = new \GuzzleHttp\Client([
|
||||||
|
'timeout' => 10,
|
||||||
|
'allow_redirects' => true,
|
||||||
|
'headers' => [
|
||||||
|
'User-Agent' => 'YakNet Accessibility Console Crawler/1.0',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
while (!empty($queue) && count($visited) < $maxPages) {
|
||||||
|
$current = array_shift($queue);
|
||||||
|
if (!is_array($current)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = $current['url'];
|
||||||
|
$depth = (int)$current['depth'];
|
||||||
|
|
||||||
|
// Remove fragment
|
||||||
|
$cleanUrl = strtok($url, '#');
|
||||||
|
if ($cleanUrl === false || isset($visited[$cleanUrl])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$visited[$cleanUrl] = true;
|
||||||
|
$io->write(" <fg=gray>Crawling [Depth $depth]:</> <fg=cyan>$cleanUrl</>... ");
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $client->request('GET', $cleanUrl);
|
||||||
|
$html = (string)$response->getBody();
|
||||||
|
$io->writeln("<fg=green>OK</>");
|
||||||
|
|
||||||
|
// Scan the page HTML
|
||||||
|
$pageViolations = $scanner->scan($html);
|
||||||
|
$results[$cleanUrl] = $pageViolations;
|
||||||
|
|
||||||
|
// Extract links if depth is less than limit
|
||||||
|
if ($depth < $depthLimit) {
|
||||||
|
$html5 = new \Masterminds\HTML5(['disable_html_ns' => true]);
|
||||||
|
$doc = $html5->loadHTML($html);
|
||||||
|
$links = $doc->getElementsByTagName('a');
|
||||||
|
|
||||||
|
foreach ($links as $link) {
|
||||||
|
$href = $link->getAttribute('href');
|
||||||
|
if (empty($href) || str_starts_with($href, 'javascript:') || str_starts_with($href, 'mailto:') || str_starts_with($href, 'tel:')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve relative URI
|
||||||
|
try {
|
||||||
|
$baseUri = new \GuzzleHttp\Psr7\Uri($cleanUrl);
|
||||||
|
$relativeUri = new \GuzzleHttp\Psr7\Uri($href);
|
||||||
|
$absoluteUri = \GuzzleHttp\Psr7\UriResolver::resolve($baseUri, $relativeUri);
|
||||||
|
$absoluteUrl = (string)$absoluteUri;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check host compatibility
|
||||||
|
$parsedAbsolute = parse_url($absoluteUrl);
|
||||||
|
$absoluteHost = $parsedAbsolute['host'] ?? '';
|
||||||
|
|
||||||
|
if ($absoluteHost === $baseHost) {
|
||||||
|
// Add to queue if not already visited or in queue
|
||||||
|
$cleanAbsolute = strtok($absoluteUrl, '#');
|
||||||
|
if ($cleanAbsolute !== false && !isset($visited[$cleanAbsolute])) {
|
||||||
|
$inQueue = false;
|
||||||
|
foreach ($queue as $q) {
|
||||||
|
if ($q['url'] === $cleanAbsolute) {
|
||||||
|
$inQueue = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$inQueue) {
|
||||||
|
$queue[] = ['url' => $cleanAbsolute, 'depth' => $depth + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$io->writeln("<fg=red>FAIL (" . $e->getMessage() . ")</>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Standard single page/file scan
|
||||||
|
$html = @file_get_contents($target);
|
||||||
|
if ($html === false || $html === '') {
|
||||||
|
$io->error("Could not read target: $target");
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
$violations = $scanner->scan($html);
|
||||||
|
$results[$target] = $violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalViolations = 0;
|
||||||
|
foreach ($results as $url => $violations) {
|
||||||
|
$totalViolations += count($violations);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($totalViolations === 0) {
|
||||||
$io->success("No violations found!");
|
$io->success("No violations found!");
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
$io->section("Found " . count($violations) . " violations");
|
$io->section("Found a total of $totalViolations violations across " . count($results) . " pages");
|
||||||
|
|
||||||
// 4. Setup Helpers
|
// 3. Setup Helpers
|
||||||
$locator = new SourceLocator($projectPath);
|
$locator = new SourceLocator($projectPath);
|
||||||
$config = new Config();
|
$config = new Config();
|
||||||
$fixer = $enableAi ? new GeminiFixer($config->getGeminiApiKey()) : null;
|
$fixer = $enableAi ? new GeminiFixer($config->getGeminiApiKey()) : null;
|
||||||
|
|
||||||
foreach ($violations as $violation) {
|
foreach ($results as $url => $violations) {
|
||||||
$io->writeln("<fg=white;bg=red;options=bold> {$violation->ruleId} </>\t<fg=red>{$violation->message}</>");
|
if (empty($violations)) {
|
||||||
|
continue;
|
||||||
// Show Snippet
|
|
||||||
$io->writeln(" <fg=gray>Snippet:</> <fg=yellow>" . htmlspecialchars($violation->htmlSnippet) . "</>");
|
|
||||||
|
|
||||||
// Source Mapping
|
|
||||||
$location = $locator->locate($violation->htmlSnippet);
|
|
||||||
if ($location) {
|
|
||||||
$io->writeln(" <fg=gray>Source:</> <fg=cyan>{$location['file']}:{$location['line']}</>");
|
|
||||||
}
|
}
|
||||||
|
$io->section("Page: $url (" . count($violations) . " violations)");
|
||||||
|
|
||||||
// AI Fixer
|
foreach ($violations as $violation) {
|
||||||
if ($enableAi && $fixer) {
|
$io->writeln("<fg=white;bg=red;options=bold> {$violation->ruleId} </>\t<fg=red>{$violation->message}</>");
|
||||||
$io->write(" <fg=gray>AI Fix Suggestion:</> ");
|
|
||||||
$suggestion = $fixer->suggestFix($violation);
|
// Show Snippet
|
||||||
if ($suggestion) {
|
$io->writeln(" <fg=gray>Snippet:</> <fg=yellow>" . htmlspecialchars($violation->htmlSnippet) . "</>");
|
||||||
$violation->fixSuggestion = $suggestion;
|
|
||||||
$io->writeln("<fg=green>" . htmlspecialchars($suggestion) . "</>");
|
// Source Mapping
|
||||||
} else {
|
$location = $locator->locate($violation->htmlSnippet);
|
||||||
$io->writeln("<fg=red>Could not generate suggestion (check API key)</>");
|
if ($location) {
|
||||||
|
$io->writeln(" <fg=gray>Source:</> <fg=cyan>{$location['file']}:{$location['line']}</>");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$io->writeln("");
|
// AI Fixer
|
||||||
|
if ($enableAi && $fixer) {
|
||||||
|
$io->write(" <fg=gray>AI Fix Suggestion:</> ");
|
||||||
|
$suggestion = $fixer->suggestFix($violation);
|
||||||
|
if ($suggestion) {
|
||||||
|
$violation->fixSuggestion = $suggestion;
|
||||||
|
$io->writeln("<fg=green>" . htmlspecialchars($suggestion) . "</>");
|
||||||
|
} else {
|
||||||
|
$io->writeln("<fg=red>Could not generate suggestion (check API key)</>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->writeln("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$io->warning("Scanned " . count($violations) . " errors. Use --ai to get fix suggestions.");
|
$io->warning("Scanned " . $totalViolations . " errors. Use --ai to get fix suggestions.");
|
||||||
|
|
||||||
// Export Report
|
// 4. Export Report
|
||||||
$reportPath = $input->getOption('report');
|
$reportPath = $input->getOption('report');
|
||||||
if (is_string($reportPath) && $reportPath !== '') {
|
if (is_string($reportPath) && $reportPath !== '') {
|
||||||
$extension = pathinfo($reportPath, PATHINFO_EXTENSION);
|
$extension = pathinfo($reportPath, PATHINFO_EXTENSION);
|
||||||
if ($extension === 'json') {
|
if ($extension === 'json') {
|
||||||
$data = array_map(fn($v) => $v->toArray(), $violations);
|
$serialized = [];
|
||||||
file_put_contents($reportPath, json_encode($data, JSON_PRETTY_PRINT));
|
foreach ($results as $url => $violations) {
|
||||||
|
$serialized[$url] = array_map(fn($v) => $v->toArray(), $violations);
|
||||||
|
}
|
||||||
|
file_put_contents($reportPath, json_encode($serialized, JSON_PRETTY_PRINT));
|
||||||
} else {
|
} else {
|
||||||
$reporter = new \YakNet\AccessibilityConsole\Reporting\HtmlDashboardReporter();
|
$reporter = new \YakNet\AccessibilityConsole\Reporting\HtmlDashboardReporter();
|
||||||
file_put_contents($reportPath, $reporter->render($violations, $target));
|
file_put_contents($reportPath, $reporter->render($results, $target));
|
||||||
}
|
}
|
||||||
$io->success("Report saved to: $reportPath");
|
$io->success("Report saved to: $reportPath");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,10 @@ class Scanner
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$doc = new \DOMDocument();
|
$html5 = new \Masterminds\HTML5([
|
||||||
// Suppress warnings for malformed HTML
|
'disable_html_ns' => true,
|
||||||
@$doc->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
]);
|
||||||
|
$doc = $html5->loadHTML($html);
|
||||||
|
|
||||||
return $this->engine->run($doc);
|
return $this->engine->run($doc);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ use YakNet\AccessibilityConsole\Core\Violation;
|
|||||||
class HtmlDashboardReporter
|
class HtmlDashboardReporter
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param Violation[] $violations
|
* @param array<string, Violation[]> $results
|
||||||
*/
|
*/
|
||||||
public function render(array $violations, string $target): string
|
public function render(array $results, string $target): string
|
||||||
{
|
{
|
||||||
$count = count($violations);
|
$totalPages = count($results);
|
||||||
$date = date('Y-m-d H:i:s');
|
$date = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
// Calculate severity and standard counts
|
// Calculate severity, standard counts and overall average score
|
||||||
$criticalCount = 0;
|
$criticalCount = 0;
|
||||||
$errorCount = 0;
|
$errorCount = 0;
|
||||||
$warningCount = 0;
|
$warningCount = 0;
|
||||||
@@ -24,110 +24,145 @@ class HtmlDashboardReporter
|
|||||||
$standardAA = 0;
|
$standardAA = 0;
|
||||||
$standardAAA = 0;
|
$standardAAA = 0;
|
||||||
|
|
||||||
foreach ($violations as $v) {
|
$pageScores = [];
|
||||||
switch ($v->severity) {
|
$totalViolations = 0;
|
||||||
case \YakNet\AccessibilityConsole\Core\Severity::CRITICAL:
|
|
||||||
$criticalCount++;
|
foreach ($results as $url => $violations) {
|
||||||
break;
|
$totalViolations += count($violations);
|
||||||
case \YakNet\AccessibilityConsole\Core\Severity::ERROR:
|
$pageCrit = 0;
|
||||||
$errorCount++;
|
$pageErr = 0;
|
||||||
break;
|
$pageWarn = 0;
|
||||||
case \YakNet\AccessibilityConsole\Core\Severity::WARNING:
|
$pageInfo = 0;
|
||||||
$warningCount++;
|
|
||||||
break;
|
foreach ($violations as $v) {
|
||||||
case \YakNet\AccessibilityConsole\Core\Severity::INFO:
|
switch ($v->severity) {
|
||||||
$infoCount++;
|
case \YakNet\AccessibilityConsole\Core\Severity::CRITICAL:
|
||||||
break;
|
$criticalCount++;
|
||||||
|
$pageCrit++;
|
||||||
|
break;
|
||||||
|
case \YakNet\AccessibilityConsole\Core\Severity::ERROR:
|
||||||
|
$errorCount++;
|
||||||
|
$pageErr++;
|
||||||
|
break;
|
||||||
|
case \YakNet\AccessibilityConsole\Core\Severity::WARNING:
|
||||||
|
$warningCount++;
|
||||||
|
$pageWarn++;
|
||||||
|
break;
|
||||||
|
case \YakNet\AccessibilityConsole\Core\Severity::INFO:
|
||||||
|
$infoCount++;
|
||||||
|
$pageInfo++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($v->standard) {
|
||||||
|
case \YakNet\AccessibilityConsole\Core\WCAGStandard::A:
|
||||||
|
$standardA++;
|
||||||
|
break;
|
||||||
|
case \YakNet\AccessibilityConsole\Core\WCAGStandard::AA:
|
||||||
|
$standardAA++;
|
||||||
|
break;
|
||||||
|
case \YakNet\AccessibilityConsole\Core\WCAGStandard::AAA:
|
||||||
|
$standardAAA++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ($v->standard) {
|
$deductions = ($pageCrit * 15) + ($pageErr * 10) + ($pageWarn * 4) + ($pageInfo * 1);
|
||||||
case \YakNet\AccessibilityConsole\Core\WCAGStandard::A:
|
$pageScores[$url] = max(0, min(100, 100 - $deductions));
|
||||||
$standardA++;
|
|
||||||
break;
|
|
||||||
case \YakNet\AccessibilityConsole\Core\WCAGStandard::AA:
|
|
||||||
$standardAA++;
|
|
||||||
break;
|
|
||||||
case \YakNet\AccessibilityConsole\Core\WCAGStandard::AAA:
|
|
||||||
$standardAAA++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate health score (0-100)
|
$averageScore = $totalPages > 0 ? (int)round(array_sum($pageScores) / $totalPages) : 100;
|
||||||
$deductions = ($criticalCount * 15) + ($errorCount * 10) + ($warningCount * 4) + ($infoCount * 1);
|
|
||||||
$score = max(0, min(100, 100 - $deductions));
|
// Generate Page Selector options
|
||||||
|
$pageOptions = "<option value=\"all\">Tüm Sayfalar ({$totalPages} sayfa, {$totalViolations} ihlal)</option>";
|
||||||
|
foreach ($results as $url => $violations) {
|
||||||
|
$escapedUrl = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
|
||||||
|
$vCount = count($violations);
|
||||||
|
$pageOptions .= "<option value=\"{$escapedUrl}\">{$escapedUrl} ({$vCount} ihlal)</option>";
|
||||||
|
}
|
||||||
|
|
||||||
$cards = '';
|
$cards = '';
|
||||||
foreach ($violations as $index => $v) {
|
$cardIndex = 0;
|
||||||
$severityClass = strtolower($v->severity->value);
|
foreach ($results as $url => $violations) {
|
||||||
$standardVal = $v->standard->value;
|
$escapedUrl = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
|
||||||
$aiSuggestion = $v->fixSuggestion;
|
foreach ($violations as $v) {
|
||||||
$explanation = '';
|
$severityClass = strtolower($v->severity->value);
|
||||||
$fixedCode = '';
|
$standardVal = $v->standard->value;
|
||||||
|
$aiSuggestion = $v->fixSuggestion;
|
||||||
|
$explanation = '';
|
||||||
|
$fixedCode = '';
|
||||||
|
|
||||||
if ($aiSuggestion && str_contains($aiSuggestion, 'FIX:')) {
|
if ($aiSuggestion && str_contains($aiSuggestion, 'FIX:')) {
|
||||||
preg_match('/EXPLANATION:(.*)FIX:(.*)/s', $aiSuggestion, $matches);
|
preg_match('/EXPLANATION:(.*)FIX:(.*)/s', $aiSuggestion, $matches);
|
||||||
$explanation = trim($matches[1] ?? '');
|
$explanation = trim($matches[1] ?? '');
|
||||||
$fixedCode = trim($matches[2] ?? '');
|
$fixedCode = trim($matches[2] ?? '');
|
||||||
$fixedCode = preg_replace('/^```html\s*|\s*```$/i', '', $fixedCode);
|
$fixedCode = preg_replace('/^```html\s*|\s*```$/i', '', $fixedCode);
|
||||||
} else {
|
} else {
|
||||||
$fixedCode = $aiSuggestion;
|
$fixedCode = $aiSuggestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
$escapedSnippet = htmlspecialchars($v->htmlSnippet, ENT_QUOTES, 'UTF-8');
|
||||||
|
$escapedFixed = $fixedCode ? htmlspecialchars($fixedCode, ENT_QUOTES, 'UTF-8') : '';
|
||||||
|
$escapedExplanation = $explanation ? htmlspecialchars($explanation, ENT_QUOTES, 'UTF-8') : '';
|
||||||
|
$locationStr = $v->location ? htmlspecialchars($v->location['file'] . ':' . $v->location['line'], ENT_QUOTES, 'UTF-8') : 'N/A';
|
||||||
|
$escapedRuleId = htmlspecialchars($v->ruleId, ENT_QUOTES, 'UTF-8');
|
||||||
|
$escapedMessage = htmlspecialchars($v->message, ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
|
$hasAiFix = !empty($fixedCode);
|
||||||
|
$aiTabButton = $hasAiFix ? "<button class=\"tab-btn\" id=\"tab-ai-{$cardIndex}\" role=\"tab\" aria-selected=\"false\" aria-controls=\"panel-ai-{$cardIndex}\" tabindex=\"-1\">Yapay Zeka Önerisi</button>" : "";
|
||||||
|
$aiTabPanel = $hasAiFix ? "
|
||||||
|
<div class=\"tab-panel\" id=\"panel-ai-{$cardIndex}\" role=\"tabpanel\" aria-labelledby=\"tab-ai-{$cardIndex}\" hidden>
|
||||||
|
<div class=\"ai-recommendation\">
|
||||||
|
" . ($escapedExplanation ? "<div class=\"ai-explanation\"><strong>Öneri Açıklaması:</strong> {$escapedExplanation}</div>" : "") . "
|
||||||
|
<div class=\"code-wrapper\">
|
||||||
|
<pre><code id=\"code-ai-{$cardIndex}\">{$escapedFixed}</code></pre>
|
||||||
|
<button class=\"copy-btn\" aria-label=\"{$escapedRuleId} kuralı için önerilen düzeltmeyi kopyala\" onclick=\"copyCode('#code-ai-{$cardIndex}', this)\">
|
||||||
|
<svg class=\"icon-copy\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>
|
||||||
|
<span>Kopyala</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>" : "";
|
||||||
|
|
||||||
|
$cards .= "
|
||||||
|
<div class=\"violation-card\" data-severity=\"{$severityClass}\" data-standard=\"{$standardVal}\" data-rule=\"{$escapedRuleId}\" data-message=\"{$escapedMessage}\" data-page=\"{$escapedUrl}\">
|
||||||
|
<div class=\"card-header\">
|
||||||
|
<div class=\"badge-row\">
|
||||||
|
<span class=\"badge rule-badge\">{$escapedRuleId}</span>
|
||||||
|
<span class=\"badge standard-badge\">WCAG {$standardVal}</span>
|
||||||
|
<span class=\"badge severity-badge severity-{$severityClass}\">
|
||||||
|
<span class=\"dot\"></span>
|
||||||
|
" . ucfirst($severityClass) . "
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class=\"location-row\">
|
||||||
|
<svg class=\"icon-location\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z\"></path><circle cx=\"12\" cy=\"10\" r=\"3\"></circle></svg>
|
||||||
|
<span>{$locationStr}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class=\"card-body\">
|
||||||
|
<h3 class=\"violation-msg\">{$escapedMessage}</h3>
|
||||||
|
|
||||||
|
<div class=\"code-viewer\">
|
||||||
|
<div class=\"tab-list\" role=\"tablist\">
|
||||||
|
<button class=\"tab-btn active\" id=\"tab-orig-{$cardIndex}\" role=\"tab\" aria-selected=\"true\" aria-controls=\"panel-orig-{$cardIndex}\">Mevcut Kod</button>
|
||||||
|
{$aiTabButton}
|
||||||
|
</div>
|
||||||
|
<div class=\"tab-panel active\" id=\"panel-orig-{$cardIndex}\" role=\"tabpanel\" aria-labelledby=\"tab-orig-{$cardIndex}\">
|
||||||
|
<pre><code>{$escapedSnippet}</code></pre>
|
||||||
|
</div>
|
||||||
|
{$aiTabPanel}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=\"page-meta-row\" style=\"margin-top: 12px; font-size: 0.8rem; color: var(--text-secondary); display: flex; align-items: center; gap: 6px;\">
|
||||||
|
<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"width:14px;height:14px;color:var(--text-muted);\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"></path><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"></path></svg>
|
||||||
|
<span>Kaynak Sayfa: <strong style=\"word-break: break-all;\">{$escapedUrl}</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>";
|
||||||
|
|
||||||
|
$cardIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
$escapedSnippet = htmlspecialchars($v->htmlSnippet, ENT_QUOTES, 'UTF-8');
|
|
||||||
$escapedFixed = $fixedCode ? htmlspecialchars($fixedCode, ENT_QUOTES, 'UTF-8') : '';
|
|
||||||
$escapedExplanation = $explanation ? htmlspecialchars($explanation, ENT_QUOTES, 'UTF-8') : '';
|
|
||||||
$locationStr = $v->location ? htmlspecialchars($v->location['file'] . ':' . $v->location['line'], ENT_QUOTES, 'UTF-8') : 'N/A';
|
|
||||||
$escapedRuleId = htmlspecialchars($v->ruleId, ENT_QUOTES, 'UTF-8');
|
|
||||||
$escapedMessage = htmlspecialchars($v->message, ENT_QUOTES, 'UTF-8');
|
|
||||||
|
|
||||||
$hasAiFix = !empty($fixedCode);
|
|
||||||
$aiTabButton = $hasAiFix ? "<button class=\"tab-btn\" id=\"tab-ai-{$index}\" role=\"tab\" aria-selected=\"false\" aria-controls=\"panel-ai-{$index}\" tabindex=\"-1\">Yapay Zeka Önerisi</button>" : "";
|
|
||||||
$aiTabPanel = $hasAiFix ? "
|
|
||||||
<div class=\"tab-panel\" id=\"panel-ai-{$index}\" role=\"tabpanel\" aria-labelledby=\"tab-ai-{$index}\" hidden>
|
|
||||||
<div class=\"ai-recommendation\">
|
|
||||||
" . ($escapedExplanation ? "<div class=\"ai-explanation\"><strong>Öneri Açıklaması:</strong> {$escapedExplanation}</div>" : "") . "
|
|
||||||
<div class=\"code-wrapper\">
|
|
||||||
<pre><code id=\"code-ai-{$index}\">{$escapedFixed}</code></pre>
|
|
||||||
<button class=\"copy-btn\" aria-label=\"{$escapedRuleId} kuralı için önerilen düzeltmeyi kopyala\" onclick=\"copyCode('#code-ai-{$index}', this)\">
|
|
||||||
<svg class=\"icon-copy\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>
|
|
||||||
<span>Kopyala</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>" : "";
|
|
||||||
|
|
||||||
$cards .= "
|
|
||||||
<div class=\"violation-card\" data-severity=\"{$severityClass}\" data-standard=\"{$standardVal}\" data-rule=\"{$escapedRuleId}\" data-message=\"{$escapedMessage}\">
|
|
||||||
<div class=\"card-header\">
|
|
||||||
<div class=\"badge-row\">
|
|
||||||
<span class=\"badge rule-badge\">{$escapedRuleId}</span>
|
|
||||||
<span class=\"badge standard-badge\">WCAG {$standardVal}</span>
|
|
||||||
<span class=\"badge severity-badge severity-{$severityClass}\">
|
|
||||||
<span class=\"dot\"></span>
|
|
||||||
" . ucfirst($severityClass) . "
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class=\"location-row\">
|
|
||||||
<svg class=\"icon-location\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z\"></path><circle cx=\"12\" cy=\"10\" r=\"3\"></circle></svg>
|
|
||||||
<span>{$locationStr}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=\"card-body\">
|
|
||||||
<h3 class=\"violation-msg\">{$escapedMessage}</h3>
|
|
||||||
|
|
||||||
<div class=\"code-viewer\">
|
|
||||||
<div class=\"tab-list\" role=\"tablist\">
|
|
||||||
<button class=\"tab-btn active\" id=\"tab-orig-{$index}\" role=\"tab\" aria-selected=\"true\" aria-controls=\"panel-orig-{$index}\">Mevcut Kod</button>
|
|
||||||
{$aiTabButton}
|
|
||||||
</div>
|
|
||||||
<div class=\"tab-panel active\" id=\"panel-orig-{$index}\" role=\"tabpanel\" aria-labelledby=\"tab-orig-{$index}\">
|
|
||||||
<pre><code>{$escapedSnippet}</code></pre>
|
|
||||||
</div>
|
|
||||||
{$aiTabPanel}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
@@ -378,7 +413,7 @@ class HtmlDashboardReporter
|
|||||||
stroke-width: 8;
|
stroke-width: 8;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-dasharray: 251.2;
|
stroke-dasharray: 251.2;
|
||||||
stroke-dashoffset: calc(251.2 - (251.2 * {$score}) / 100);
|
stroke-dashoffset: calc(251.2 - (251.2 * {$averageScore}) / 100);
|
||||||
transition: stroke-dashoffset 1.5s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: stroke-dashoffset 1.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +501,7 @@ class HtmlDashboardReporter
|
|||||||
|
|
||||||
.search-group {
|
.search-group {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 2;
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,6 +541,35 @@ class HtmlDashboardReporter
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-select-group {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-input {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid var(--border-card);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
body.light-theme .select-input {
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
body.light-theme .select-input option {
|
||||||
|
background-color: white;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
body.light-theme .select-input:focus {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.filter-group {
|
.filter-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -820,7 +884,7 @@ class HtmlDashboardReporter
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-meta">
|
<div class="header-meta">
|
||||||
<div class="meta-tag" title="{$target}">Hedef: {$target}</div>
|
<div class="meta-tag" title="{$target}">Hedef Site: {$target}</div>
|
||||||
<div class="meta-tag">Tarih: {$date}</div>
|
<div class="meta-tag">Tarih: {$date}</div>
|
||||||
<button id="theme-toggle" class="theme-btn" aria-label="Açık/Karanlık tema değiştir" onclick="toggleTheme()">
|
<button id="theme-toggle" class="theme-btn" aria-label="Açık/Karanlık tema değiştir" onclick="toggleTheme()">
|
||||||
<!-- Moon icon -->
|
<!-- Moon icon -->
|
||||||
@@ -838,13 +902,13 @@ class HtmlDashboardReporter
|
|||||||
<div class="gauge-container">
|
<div class="gauge-container">
|
||||||
<svg viewBox="0 0 100 100" class="gauge">
|
<svg viewBox="0 0 100 100" class="gauge">
|
||||||
<circle cx="50" cy="50" r="40" class="gauge-bg"></circle>
|
<circle cx="50" cy="50" r="40" class="gauge-bg"></circle>
|
||||||
<circle cx="50" cy="50" r="40" class="gauge-val " id="score-gauge" stroke-dasharray="251.2" stroke-dashoffset="251.2"></circle>
|
<circle cx="50" cy="50" r="40" class="gauge-val" id="score-gauge" stroke-dasharray="251.2" stroke-dashoffset="251.2"></circle>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="gauge-label" id="score-val">0</div>
|
<div class="gauge-label" id="score-val">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-desc">
|
<div class="metric-desc">
|
||||||
<h2>Erişilebilirlik Skoru</h2>
|
<h2>Ortalama Sağlık Skoru</h2>
|
||||||
<p>Sayfanın genel standartlara uygunluk derecesi.</p>
|
<p>Tarama yapılan sayfaların ortalama puanı.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -882,20 +946,16 @@ class HtmlDashboardReporter
|
|||||||
<div class="glass-panel">
|
<div class="glass-panel">
|
||||||
<div class="summary-stats">
|
<div class="summary-stats">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-label">WCAG A</span>
|
<span class="stat-label">Taranan Sayfa</span>
|
||||||
<span class="stat-val">{$standardA}</span>
|
<span class="stat-val">{$totalPages}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-label">WCAG AA</span>
|
<span class="stat-label">WCAG A / AA / AAA</span>
|
||||||
<span class="stat-val">{$standardAA}</span>
|
<span class="stat-val">{$standardA} / {$standardAA} / {$standardAAA}</span>
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-label">WCAG AAA</span>
|
|
||||||
<span class="stat-val">{$standardAAA}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-label">Toplam İhlal</span>
|
<span class="stat-label">Toplam İhlal</span>
|
||||||
<span class="stat-val">{$count}</span>
|
<span class="stat-val">{$totalViolations}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -909,6 +969,12 @@ class HtmlDashboardReporter
|
|||||||
</svg>
|
</svg>
|
||||||
<input type="search" id="search-box" class="search-input" placeholder="Kural veya hata açıklaması ara..." aria-label="Kural veya hata açıklaması ara" oninput="applyFilters()">
|
<input type="search" id="search-box" class="search-input" placeholder="Kural veya hata açıklaması ara..." aria-label="Kural veya hata açıklaması ara" oninput="applyFilters()">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="page-select-group">
|
||||||
|
<select id="page-select" class="select-input" aria-label="Taranan sayfaya göre filtrele" onchange="applyFilters()">
|
||||||
|
{$pageOptions}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<button class="filter-btn active" id="btn-all" onclick="setSeverityFilter('all')" aria-label="Tüm ihlalleri göster" aria-pressed="true">Hepsi</button>
|
<button class="filter-btn active" id="btn-all" onclick="setSeverityFilter('all')" aria-label="Tüm ihlalleri göster" aria-pressed="true">Hepsi</button>
|
||||||
@@ -946,7 +1012,7 @@ class HtmlDashboardReporter
|
|||||||
<!-- JavaScript Logic -->
|
<!-- JavaScript Logic -->
|
||||||
<script>
|
<script>
|
||||||
// Apply Circular Score Gauge Color and Animation
|
// Apply Circular Score Gauge Color and Animation
|
||||||
const score = {$score};
|
const score = {$averageScore};
|
||||||
const gauge = document.getElementById('score-gauge');
|
const gauge = document.getElementById('score-gauge');
|
||||||
const scoreText = document.getElementById('score-val');
|
const scoreText = document.getElementById('score-val');
|
||||||
|
|
||||||
@@ -1143,6 +1209,7 @@ class HtmlDashboardReporter
|
|||||||
|
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
const searchVal = document.getElementById('search-box').value.toLowerCase();
|
const searchVal = document.getElementById('search-box').value.toLowerCase();
|
||||||
|
const pageVal = document.getElementById('page-select').value;
|
||||||
const cards = document.querySelectorAll('.violation-card');
|
const cards = document.querySelectorAll('.violation-card');
|
||||||
let visibleCount = 0;
|
let visibleCount = 0;
|
||||||
|
|
||||||
@@ -1150,11 +1217,13 @@ class HtmlDashboardReporter
|
|||||||
const severity = card.getAttribute('data-severity');
|
const severity = card.getAttribute('data-severity');
|
||||||
const rule = card.getAttribute('data-rule').toLowerCase();
|
const rule = card.getAttribute('data-rule').toLowerCase();
|
||||||
const message = card.getAttribute('data-message').toLowerCase();
|
const message = card.getAttribute('data-message').toLowerCase();
|
||||||
|
const page = card.getAttribute('data-page');
|
||||||
|
|
||||||
const matchesSeverity = (activeSeverity === 'all' || severity === activeSeverity);
|
const matchesSeverity = (activeSeverity === 'all' || severity === activeSeverity);
|
||||||
const matchesSearch = (!searchVal || rule.includes(searchVal) || message.includes(searchVal));
|
const matchesSearch = (!searchVal || rule.includes(searchVal) || message.includes(searchVal));
|
||||||
|
const matchesPage = (pageVal === 'all' || page === pageVal);
|
||||||
|
|
||||||
if (matchesSeverity && matchesSearch) {
|
if (matchesSeverity && matchesSearch && matchesPage) {
|
||||||
card.style.display = '';
|
card.style.display = '';
|
||||||
visibleCount++;
|
visibleCount++;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user