Izvorni PHP: Otkrivanje tehnološkog okruženja klijentske web-stranice za pametnije ponude redizajna
Ponuda za redizajn postaje rizična kada se ispostavi da „prilično jednostavna stranica” sadržava alat za izradu stranica, zastarjeli dodatak za e-trgovinu, nekoliko analitičkih oznaka i lanac preusmjeravanja koji nitko nije dokumentirao. Vizualni opseg čini samo polovicu procjene. Postojeći skup tehnologija određuje napor migracije, opseg testiranja, mogućnosti izdvajanja sadržaja i vjerojatnost neugodnih iznenađenja.
Ovaj vodič izrađuje nativni alat naredbenog retka u PHP-u 8.3 koji šalje javni URL klijenta API-ju Website Technology Detector, pretvara nalaze s ocjenama pouzdanosti u domenske objekte i stvara strukturirane dokaze za pripremu ponude. Integracija koristi nativni cURL, ograničeni broj ponovnih pokušaja, obrambenu obradu odgovora, PHPUnit testove i tajne pohranjene u varijablama okruženja.
Dobijte pristup i kopirajte servisni token
Izradite račun na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako ga već imate.
- Otvorite stranicu usluge Website Technology Detector.
- Odaberite dostupni plan Free, Plus ili Pro i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite panel Service token i kopirajte token ograničen na tu uslugu.
- Pohranite ga u konfiguraciju okruženja projekta, nikada u izvorni kod PHP-a.
Ova usluga zahtijeva autentikaciju. Prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Upotrijebit ćemo oblik Bearer jer se parametri upita često pojavljuju u zapisnicima pristupa i nadzornim zapisima. Po zahtjevu šaljite samo jedan mehanizam autentikacije.
Ponovno generiranje servisnog tokena opoziva prethodno aktivni token. Ponovno generiranje tretirajte kao rotaciju vjerodajnica: ažurirajte svako implementirano okruženje, ponovno pokrenite pogođene procese, provjerite novi token i tek tada smatrajte rotaciju dovršenom.
Potvrdite točan API poziv
Integracija koristi POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Njegovo JSON tijelo zahtjeva sadržava jednu obaveznu integracijsku vrijednost: url.
Pokrenite ovaj minimalni zahtjev nakon što zamijenite rezervirano mjesto u okruženju ljuske:
export WEBSITE_TECH_DETECTOR_TOKEN='YOUR_SERVICE_TOKEN'
curl --fail-with-body \
--connect-timeout 5 \
--max-time 20 \
-X POST \
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies' \
-H "Authorization: Bearer ${WEBSITE_TECH_DETECTOR_TOKEN}" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{"url":"https://www.example.com"}'
Uspješan odgovor sadržava detekcije tehnologija s pouzdanošću, popratnim dokazima, informacijama o verziji gdje su dostupne i informacijama o preusmjeravanju. Nemojte pretpostaviti da svaka tehnologija otkriva verziju, da je dokaz uvijek skalaran ili da će budući odgovori sadržavati samo polja koja vaša aplikacija već razumije.
Oblikujte projekt oko jedne granice
Alat ima tri sloja: transport odgovoran za HTTP mehaniku, klijent odgovoran za autentikaciju i pravila ponovnih pokušaja te domenske objekte odgovorne za normalizaciju odgovora. Tijek rada za ponudu koristi domenske objekte umjesto da ovisi o sirovom JSON-u.
stack-inspector/
├── bin/inspect.php
├── src/
│ ├── Contract.php
│ ├── CurlTransport.php
│ ├── DetectorClient.php
│ └── DetectionReport.php
├── tests/DetectorClientTest.php
├── composer.json
└── .env.local
Izradite composer.json s PHP-om 8.3, proširenjem cURL, PSR-4 automatskim učitavanjem i PHPUnitom 11:
{
"require": {
"php": "^8.3",
"ext-curl": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"QuoteStack\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"QuoteStack\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit tests"
}
}
Pokrenite composer install. Za lokalni razvoj smjestite ovo u nepredanu datoteku .env.local:
export WEBSITE_TECH_DETECTOR_TOKEN='YOUR_SERVICE_TOKEN'
export WEBSITE_TECH_CONNECT_TIMEOUT_MS='5000'
export WEBSITE_TECH_RESPONSE_TIMEOUT_MS='20000'
Učitajte je s set -a; . ./.env.local; set +a. Dodajte .env.local u .gitignore. Nativni PHP ne raščlanjuje automatski datoteke okruženja; produkcija treba unijeti iste varijable putem upravitelja procesa, platformske infrastrukture za kontejnere ili upravitelja tajnama.
Implementirajte ograničeni nativni cURL transport
Transport ne zna što znači detekcija tehnologije. Vraća status, zaglavlja i tijelo dok provodi TLS provjeru, vremenska ograničenja povezivanja i ukupnog trajanja te ograničenje odgovora od dva megabajta.
<?php
// src/Contract.php
declare(strict_types=1);
namespace QuoteStack;
final readonly class HttpResponse
{
public function __construct(
public int $status,
public array $headers,
public string $body,
) {}
}
interface Transport
{
public function post(
string $url,
array $headers,
string $body,
int $connectTimeoutMs,
int $responseTimeoutMs,
): HttpResponse;
}
final class TransportException extends \RuntimeException {}
final class ApiFailure extends \RuntimeException
{
public function __construct(
public readonly string $kind,
public readonly ?int $status = null,
string $message = 'Technology detection failed',
?\Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
}
<?php
// src/CurlTransport.php
declare(strict_types=1);
namespace QuoteStack;
final class CurlTransport implements Transport
{
public function __construct(private int $maxBytes = 2_097_152) {}
public function post(
string $url,
array $headers,
string $body,
int $connectTimeoutMs,
int $responseTimeoutMs,
): HttpResponse {
$curl = curl_init($url);
if ($curl === false) {
throw new TransportException('Could not initialize cURL');
}
$responseHeaders = [];
$responseBody = '';
$tooLarge = false;
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_CONNECTTIMEOUT_MS => $connectTimeoutMs,
CURLOPT_TIMEOUT_MS => $responseTimeoutMs,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HEADERFUNCTION => static function ($handle, string $line)
use (&$responseHeaders): int {
$length = strlen($line);
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return $length;
},
CURLOPT_WRITEFUNCTION => function ($handle, string $chunk)
use (&$responseBody, &$tooLarge): int {
if (strlen($responseBody) + strlen($chunk) > $this->maxBytes) {
$tooLarge = true;
return 0;
}
$responseBody .= $chunk;
return strlen($chunk);
},
]);
$result = curl_exec($curl);
$status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$error = curl_error($curl);
curl_close($curl);
if ($tooLarge) {
throw new TransportException('API response exceeded the size limit');
}
if ($result === false) {
throw new TransportException('cURL request failed: ' . $error);
}
return new HttpResponse($status, $responseHeaders, $responseBody);
}
}
Normalizirajte nesigurne podatke odgovora na granici
Ugovor API-ja opisuje detekcije, pouzdanost, dokaze, verzije i preusmjeravanja. Donji preslikavač namjerno izbjegava pretpostavku o jednoj omotnici najviše razine. Prolazi dekodirani sadržaj, prepoznaje objekte oblika detekcije, čuva vrijednosti dokaza i verzije bez prisilnog mijenjanja njihova značenja, prikuplja strukture povezane s preusmjeravanjem i zadržava sirovi odgovor za kontroliranu dijagnostiku.
<?php
// src/DetectionReport.php
declare(strict_types=1);
namespace QuoteStack;
final readonly class TechnologyDetection implements \JsonSerializable
{
public function __construct(
public string $technology,
public float $confidence,
public array $evidence,
public array $versions,
) {}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}
final readonly class DetectionReport implements \JsonSerializable
{
public function __construct(
public array $detections,
public array $redirectInformation,
public array $raw,
) {}
public static function fromApi(array $payload): self
{
$detections = [];
$redirects = [];
self::walk($payload, $detections, $redirects);
return new self($detections, $redirects, $payload);
}
private static function walk(
array $node,
array &$detections,
array &$redirects,
): void {
$name = $node['technology'] ?? $node['name'] ?? null;
$confidence = $node['confidence'] ?? null;
if (is_string($name) && $name !== '' && is_numeric($confidence)) {
$detections[] = new TechnologyDetection(
$name,
(float) $confidence,
self::values($node['evidence'] ?? null),
self::values($node['versions'] ?? $node['version'] ?? null),
);
}
foreach ($node as $key => $value) {
if (is_string($key)
&& str_contains(strtolower($key), 'redirect')
&& is_array($value)
) {
$redirects[$key] = $value;
}
if (is_array($value)) {
self::walk($value, $detections, $redirects);
}
}
}
private static function values(mixed $value): array
{
if ($value === null || $value === '') {
return [];
}
return is_array($value) ? array_values($value) : [$value];
}
public function jsonSerialize(): array
{
return [
'detections' => $this->detections,
'redirect_information' => $this->redirectInformation,
];
}
}
Pouzdanost ostaje brojčana vrijednost koju vraća usluga. Kod je ne tumači prešutno kao postotak ili vjerojatnost. Pravila za ponudu trebaju upotrebljavati pragove tek nakon potvrde dokumentirane ljestvice i testiranja reprezentativnih odgovora.
Dodajte autentikaciju, ponovne pokušaje i kategorije neuspjeha
Prolazni kvarovi transporta, 429 i odabrani kvarovi pristupnika ili dostupnosti mogu se pokušati ponovno. Neispravni zahtjevi i neuspjesi autentikacije trebali bi se odmah vratiti: ponavljanje istih vjerodajnica ili sadržaja samo gubi vrijeme i kvotu.
<?php
// src/DetectorClient.php
declare(strict_types=1);
namespace QuoteStack;
final class DetectorClient
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies';
private \Closure $sleep;
public function __construct(
private Transport $transport,
private string $token,
private int $connectTimeoutMs = 5000,
private int $responseTimeoutMs = 20000,
private int $maxAttempts = 3,
?\Closure $sleep = null,
) {
if ($token === '') {
throw new \InvalidArgumentException('Service token is missing');
}
$this->sleep = $sleep ?? static fn (int $ms) => usleep($ms * 1000);
}
public function detect(string $targetUrl): DetectionReport
{
$this->assertAllowedUrl($targetUrl);
$body = json_encode(['url' => $targetUrl], JSON_THROW_ON_ERROR);
for ($attempt = 1; $attempt <= $this->maxAttempts; $attempt++) {
try {
$response = $this->transport->post(
self::ENDPOINT,
[
'Authorization: Bearer ' . $this->token,
'Accept: application/json',
'Content-Type: application/json',
],
$body,
$this->connectTimeoutMs,
$this->responseTimeoutMs,
);
} catch (TransportException $exception) {
if ($attempt === $this->maxAttempts) {
throw new ApiFailure(
'transport',
null,
'Transport attempts exhausted',
$exception,
);
}
($this->sleep)($this->backoffMs($attempt, null));
continue;
}
if ($response->status >= 200 && $response->status < 300) {
try {
$payload = json_decode(
$response->body,
true,
512,
JSON_THROW_ON_ERROR,
);
} catch (\JsonException $exception) {
throw new ApiFailure(
'invalid_response',
$response->status,
'API returned malformed JSON',
$exception,
);
}
if (!is_array($payload)) {
throw new ApiFailure(
'invalid_response',
$response->status,
'API response was not a JSON object or array',
);
}
return DetectionReport::fromApi($payload);
}
if ($response->status === 401 || $response->status === 403) {
throw new ApiFailure('authentication', $response->status);
}
$retryable = $response->status === 429
|| in_array($response->status, [502, 503, 504], true);
if (!$retryable) {
throw new ApiFailure('request', $response->status);
}
if ($attempt === $this->maxAttempts) {
$kind = $response->status === 429 ? 'rate_limit' : 'upstream';
throw new ApiFailure($kind, $response->status);
}
$retryAfter = $response->headers['retry-after'] ?? null;
($this->sleep)($this->backoffMs($attempt, $retryAfter));
}
throw new ApiFailure('internal');
}
private function backoffMs(int $attempt, mixed $retryAfter): int
{
if (is_string($retryAfter) && ctype_digit($retryAfter)) {
return min(5000, (int) $retryAfter * 1000);
}
return min(2000, 200 * (2 ** ($attempt - 1)) + random_int(0, 100));
}
private function assertAllowedUrl(string $url): void
{
$parts = parse_url($url);
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
$host = strtolower((string) ($parts['host'] ?? ''));
if (!filter_var($url, FILTER_VALIDATE_URL)
|| !in_array($scheme, ['http', 'https'], true)
|| $host === ''
|| isset($parts['user'])
|| isset($parts['pass'])
|| $host === 'localhost'
|| str_ends_with($host, '.local')
) {
throw new \InvalidArgumentException('A public HTTP(S) URL is required');
}
if (filter_var($host, FILTER_VALIDATE_IP)
&& !filter_var(
$host,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE,
)
) {
throw new \InvalidArgumentException('Private IP targets are not allowed');
}
}
}
Povežite detektor s tijekom rada za ponudu
Naredba ispisuje normalizirani JSON koji se može priložiti zapisu ponude ili ga procjenitelj može pregledati. Bilježi samo operativne metapodatke: identifikator korelacije, naziv glavnog računala, trajanje, broj i kategoriju neuspjeha. Tokeni, tijela odgovora, dokazi i potpuni URL-ovi ne ulaze u zapisnike.
<?php
// bin/inspect.php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use QuoteStack\ApiFailure;
use QuoteStack\CurlTransport;
use QuoteStack\DetectorClient;
$url = $argv[1] ?? '';
$token = getenv('WEBSITE_TECH_DETECTOR_TOKEN') ?: '';
$connectMs = (int) (getenv('WEBSITE_TECH_CONNECT_TIMEOUT_MS') ?: 5000);
$responseMs = (int) (getenv('WEBSITE_TECH_RESPONSE_TIMEOUT_MS') ?: 20000);
$requestId = bin2hex(random_bytes(8));
$started = hrtime(true);
try {
$client = new DetectorClient(
new CurlTransport(),
$token,
$connectMs,
$responseMs,
);
$report = $client->detect($url);
error_log(json_encode([
'event' => 'technology_detection_completed',
'request_id' => $requestId,
'host' => parse_url($url, PHP_URL_HOST),
'detections' => count($report->detections),
'duration_ms' => (int) ((hrtime(true) - $started) / 1_000_000),
], JSON_THROW_ON_ERROR));
echo json_encode(
$report,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR,
) . PHP_EOL;
} catch (ApiFailure | \InvalidArgumentException $exception) {
error_log(json_encode([
'event' => 'technology_detection_failed',
'request_id' => $requestId,
'kind' => $exception instanceof ApiFailure
? $exception->kind
: 'validation',
'status' => $exception instanceof ApiFailure
? $exception->status
: null,
], JSON_THROW_ON_ERROR));
fwrite(STDERR, "Inspection failed; reference {$requestId}." . PHP_EOL);
exit(1);
}
Pokrenite php bin/inspect.php https://www.example.com. Izvješće može potaknuti pitanja o migraciji CMS-a, prikazivanju na poslužiteljskoj strani, integracijama e-trgovine, zamjeni analitike, očuvanju preusmjeravanja i tehnologijama čije verzije koje nedostaju zahtijevaju ručnu istragu. Detekcija je dokaz za određivanje opsega, a ne dopuštenje za zaključivanje o privatnoj infrastrukturi ili zamjena za razgovor s dionicima.
Testirajte bez pozivanja plaćene usluge
Lažni transport čini ponašanje ponovnih pokušaja i preslikavanja determinističkim. Testovi nikada ne smiju sadržavati stvarni token ni kontaktirati produkcijsku krajnju točku.
<?php
// tests/DetectorClientTest.php
declare(strict_types=1);
namespace QuoteStack\Tests;
use PHPUnit\Framework\TestCase;
use QuoteStack\ApiFailure;
use QuoteStack\DetectorClient;
use QuoteStack\HttpResponse;
use QuoteStack\Transport;
final class FakeTransport implements Transport
{
public int $calls = 0;
public function __construct(private array $responses) {}
public function post(
string $url,
array $headers,
string $body,
int $connectTimeoutMs,
int $responseTimeoutMs,
): HttpResponse {
$this->calls++;
return array_shift($this->responses);
}
}
final class DetectorClientTest extends TestCase
{
public function testRetriesAndMapsContractData(): void
{
$transport = new FakeTransport([
new HttpResponse(503, [], '{}'),
new HttpResponse(200, [], json_encode([
'result' => [
'detections' => [[
'technology' => 'Example CMS',
'confidence' => 0.94,
'evidence' => ['generator metadata'],
'versions' => ['8'],
]],
'redirects' => [
['from' => 'http://example.test',
'to' => 'https://example.test'],
],
],
], JSON_THROW_ON_ERROR)),
]);
$client = new DetectorClient(
$transport,
'test-token',
maxAttempts: 2,
sleep: static fn (int $ms) => null,
);
$report = $client->detect('https://example.test');
self::assertSame(2, $transport->calls);
self::assertCount(1, $report->detections);
self::assertSame(
'Example CMS',
$report->detections[0]->technology,
);
self::assertNotEmpty($report->redirectInformation);
}
public function testDoesNotRetryAuthenticationFailure(): void
{
$transport = new FakeTransport([
new HttpResponse(401, [], '{}'),
]);
$client = new DetectorClient($transport, 'bad-token');
try {
$client->detect('https://example.test');
self::fail('Expected ApiFailure');
} catch (ApiFailure $failure) {
self::assertSame('authentication', $failure->kind);
self::assertSame(1, $transport->calls);
}
}
}
Pokrenite composer test. Dodajte fixturee kopirane iz odobrenih, sanitiziranih API odgovora kako se ugovor razvija, posebno za prazne detekcije, verzije koje nedostaju, višestruka preusmjeravanja, neuobičajene strukture dokaza, neispravan JSON, vremenska ograničenja i ograničavanje brzine.
Produkcijske odluke koje štite proces pripreme ponude
Sigurnost i privatnost
Prihvaćajte samo javne HTTP ili HTTPS ciljeve, ovlastite osobe koje smiju poslati inspekcije i ograničite brzinu vlastite rute ili pokretača naredbi. Lokalna validacija blokira očite privatne literalne adrese, ali DNS nazivi mogu promijeniti razrješenje; udaljena usluga mora ostati odgovorna za sigurno dohvaćanje samo javnih web-stranica. Nikada ne izlažite detektor kao neograničeni javni proxy.
Ograničite vidljivost tajni na proces aplikacije, rotirajte ih promišljeno i posvuda redigujte autorizacijska zaglavlja. Zadržavajte sirove odgovore samo kada je potrebno, uz kontrole pristupa i pravilo brisanja. URL klijenta može sam po sebi biti komercijalno osjetljiv prije nego što se najavi redizajn.
Praćenje i kvote
Pratite uspjeh, trajanje, kategoriju neuspjeha, HTTP status, broj ponovnih pokušaja i broj detekcija. Upozorite na trajne neuspjehe autentikacije jer često upućuju na istekao ili ponovno generiran token. Ponovljene odgovore 429 tretirajte kao povratnu informaciju o kapacitetu, a ne kao dopuštenje za beskonačne ponovne pokušaje. Ako priprema ponude postane interaktivna, prikažite stanje „privremeno nedostupno” i omogućite namjeran kasniji ponovni pokušaj.
Implementacija
Provjerite PHP 8.3 ili noviji, ext-curl, CA certifikate, odlazni HTTPS pristup, Composerov optimizirani automatski učitavač i ubrizgane varijable okruženja. Vrijednosti vremenskog ograničenja povezivanja i ukupnog trajanja držite ispod vlastitog ograničenja izvršavanja radnika koji poziva. Ako se inspekcije kasnije premjeste u pozadinske poslove, zadržite idempotentne identifikatore poslova i ograničite pokušaje ponovne isporuke; dodavanje reda nije potrebno za ovaj fokusirani tijek rada naredbenog retka.
Uobičajeni neuspjesi i njihovo značenje
- 401 ili 403: token nedostaje, neispravan je, ima pogrešan opseg ili je opozvan ponovnim generiranjem. Nemojte automatski pokušavati ponovno.
- Neuspjeh zahtjeva klase 400: provjerite javni URL i JSON zahtjev. Ponavljanje nepromijenjenog neispravnog zahtjeva ne može ga popraviti.
- 429: poštujte brojčanu vrijednost
Retry-Afterunutar lokalnog ograničenja, a zatim zaustavite nakon ograničenog broja pokušaja. - 502, 503 ili 504: upotrijebite kratki eksponencijalni povratni odmak s varijacijom i zadržite strukturirani neuspjeh uzvodne usluge ako su pokušaji iscrpljeni.
- Neispravan ili prevelik JSON: prekinite sigurno. Nesiguran odgovor ne pretvarajte u prazan, naizgled uspješan izvještaj.
- Nisu otkrivene tehnologije: zabilježite da je otkrivanje bilo neuvjerljivo i zakažite ručnu inspekciju umjesto pretpostavke da web-stranica nema značajan tehnološki skup.
Završni kontrolni popis za provjeru
- Upotrebljava se točna POST krajnja točka s JSON vrijednošću
url. - Token ograničen na uslugu dolazi iz okruženja i nikada se ne pojavljuje u zapisnicima ili fixtureima.
- Ograničenja povezivanja, odgovora, veličine tijela, ponovnih pokušaja i povratnog odmaka su ograničena.
- Neuspjesi autentikacije i validacije ne pokušavaju se ponovno.
- Detekcije čuvaju pouzdanost, dokaze i verzije bez izmišljenih zadanih vrijednosti.
- Informacije o preusmjeravanju zadržavaju se za planiranje migracije.
- PHPUnit u potpunosti prolazi putem lažnog transporta.
- Stvarni dimni test stvara izvješće za ovlašteni javni URL klijenta.
- Procjenitelj pregledava nalaze s malo dokaza i verzijama koje nedostaju prije određivanja cijene redizajna.
Snažna ponuda za redizajn ne pretvara se da je neizvjesnost nestala. Ona čini neizvjesnost vidljivom dovoljno rano da se otkrivanje, migracija i testiranje mogu pošteno procijeniti. Održavanjem stroge granice API-ja i čitljivosti dobivenih dokaza, ovaj mali PHP alat pretvara neprozirnu web-stranicu u bolji tehnički razgovor — i ponudu za koju je mnogo manja vjerojatnost da će se raspasti nakon početka rada.