From 0d364a9f0a8b71998bad4d361de541982a0e5f4d Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Fri, 12 Jun 2026 11:32:55 +0200 Subject: [PATCH] Remove vendored url-shortener tree before submodule. Standalone repo: git://f0xx.org/androidcast_project/url-shortener Co-authored-by: Cursor --- backend/url-shortener/DEPLOY.md | 72 ----- backend/url-shortener/README.md | 48 ---- backend/url-shortener/config/.gitignore | 1 - .../url-shortener/config/config.example.php | 20 -- backend/url-shortener/config/config.php | 20 -- .../deploy/nginx.be-url-shortener.fragment | 29 -- .../deploy/nginx.fe-s.f0xx.org.snippet | 48 ---- backend/url-shortener/openapi.yaml | 253 ------------------ backend/url-shortener/public/index.php | 72 ----- backend/url-shortener/scripts/demo_curl.sh | 55 ---- .../scripts/init-mariadb-grants.sh | 20 -- .../scripts/purge_url_shortener.php | 15 -- .../url-shortener/scripts/run-php-tests.sh | 5 - backend/url-shortener/scripts/seed_bearer.php | 26 -- .../url-shortener/scripts/smoke_shorten.sh | 28 -- backend/url-shortener/sql/schema.mariadb.sql | 62 ----- backend/url-shortener/src/AuditLog.php | 18 -- .../url-shortener/src/BearerRepository.php | 68 ----- backend/url-shortener/src/Database.php | 32 --- backend/url-shortener/src/JsonResponse.php | 23 -- backend/url-shortener/src/LinkRepository.php | 109 -------- backend/url-shortener/src/QrHandler.php | 50 ---- backend/url-shortener/src/RedirectHandler.php | 48 ---- backend/url-shortener/src/ShortenService.php | 113 -------- .../url-shortener/src/SignerRepository.php | 33 --- backend/url-shortener/src/SlugGenerator.php | 19 -- backend/url-shortener/src/UrlNormalizer.php | 60 ----- backend/url-shortener/src/bootstrap.php | 34 --- backend/url-shortener/tests/run_tests.php | 35 --- 29 files changed, 1416 deletions(-) delete mode 100644 backend/url-shortener/DEPLOY.md delete mode 100644 backend/url-shortener/README.md delete mode 100644 backend/url-shortener/config/.gitignore delete mode 100644 backend/url-shortener/config/config.example.php delete mode 100644 backend/url-shortener/config/config.php delete mode 100644 backend/url-shortener/deploy/nginx.be-url-shortener.fragment delete mode 100644 backend/url-shortener/deploy/nginx.fe-s.f0xx.org.snippet delete mode 100644 backend/url-shortener/openapi.yaml delete mode 100644 backend/url-shortener/public/index.php delete mode 100755 backend/url-shortener/scripts/demo_curl.sh delete mode 100755 backend/url-shortener/scripts/init-mariadb-grants.sh delete mode 100755 backend/url-shortener/scripts/purge_url_shortener.php delete mode 100755 backend/url-shortener/scripts/run-php-tests.sh delete mode 100755 backend/url-shortener/scripts/seed_bearer.php delete mode 100755 backend/url-shortener/scripts/smoke_shorten.sh delete mode 100644 backend/url-shortener/sql/schema.mariadb.sql delete mode 100644 backend/url-shortener/src/AuditLog.php delete mode 100644 backend/url-shortener/src/BearerRepository.php delete mode 100644 backend/url-shortener/src/Database.php delete mode 100644 backend/url-shortener/src/JsonResponse.php delete mode 100644 backend/url-shortener/src/LinkRepository.php delete mode 100644 backend/url-shortener/src/QrHandler.php delete mode 100644 backend/url-shortener/src/RedirectHandler.php delete mode 100644 backend/url-shortener/src/ShortenService.php delete mode 100644 backend/url-shortener/src/SignerRepository.php delete mode 100644 backend/url-shortener/src/SlugGenerator.php delete mode 100644 backend/url-shortener/src/UrlNormalizer.php delete mode 100644 backend/url-shortener/src/bootstrap.php delete mode 100644 backend/url-shortener/tests/run_tests.php diff --git a/backend/url-shortener/DEPLOY.md b/backend/url-shortener/DEPLOY.md deleted file mode 100644 index 13df4bb..0000000 --- a/backend/url-shortener/DEPLOY.md +++ /dev/null @@ -1,72 +0,0 @@ -# URL shortener — deploy (FE → BE) - -**Topology:** same as [INFRA.md](../../docs/INFRA.md) — TLS on Gentoo FE, app on Alpine BE `:80`. - -## Prerequisites - -| Item | Status | -|------|--------| -| DNS `s.f0xx.org` → FE | Done | -| FE TLS + BE nginx | Done | -| MariaDB `url_shortener` | Done | -| DB user | **`androidcast`** (same password as crashes `config.php`) | - -## Sync to BE - -```bash -# From dev machine (example — adjust paths) -rsync -av backend/url-shortener/ alpine-be:/var/www/localhost/htdocs/apps/s/ - -# On BE -cp /var/www/localhost/htdocs/apps/s/config/config.example.php \ - /var/www/localhost/htdocs/apps/s/config/config.php -# set db.mysql.password = same as crash reporter - -apk add libqrencode-tools # QR PNG (/api/v1/qr/…) -rc-service php-fpm81 restart -``` - -## MariaDB (one-time) - -```bash -mysql -u root -p < sql/schema.mariadb.sql -./scripts/init-mariadb-grants.sh -php scripts/seed_bearer.php --label=prod # save printed bearer token -``` - -Optional cron (purge expired links + audit >90d): - -```cron -0 4 * * * php81 /var/www/localhost/htdocs/apps/s/scripts/purge_url_shortener.php -``` - -## FE (Gentoo) - -See [deploy/nginx.fe-s.f0xx.org.snippet](deploy/nginx.fe-s.f0xx.org.snippet) — full URI → BE `:80`. - -## BE nginx - -See [deploy/nginx.be-url-shortener.fragment](deploy/nginx.be-url-shortener.fragment). - -**Quoted regex required:** `location ~ "^/[a-f0-9]{6,16}$"` - -## Verify - -```bash -curl -fsS https://s.f0xx.org/api/v1/health -export BEARER='…' ./scripts/smoke_shorten.sh -./scripts/demo_curl.sh -examples/crash_reporter/backend/scripts/validate_be_services.sh -``` - -## Hub admin (Short links card) - -Operator UI lives in the **crashes console** (shared `ac_crash_sess`), not on `s.f0xx.org`: - -- `https://apps.f0xx.org/app/androidcast_project/crashes/?view=short_links` -- Enable in crashes `config.php`: `url_shortener.enabled => true` + MariaDB `url_shortener` block (see `examples/crash_reporter/backend/config/config.example.php`) -- Smoke: `examples/crash_reporter/backend/scripts/test_short_links_api.sh` - -## Config - -[config/config.example.php](config/config.example.php) — prefer **`socket`** `/run/mysqld/mysqld.sock` on Alpine (same as crashes when TCP refused). diff --git a/backend/url-shortener/README.md b/backend/url-shortener/README.md deleted file mode 100644 index 6ac6716..0000000 --- a/backend/url-shortener/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# URL shortener (`s.f0xx.org`) - -Standalone opt-in service — **not** part of `examples/crash_reporter/backend/`. - -| Doc | Path | -|-----|------| -| SPEC | [docs/specs/20100611_3_url_shortener.md](../../docs/specs/20100611_3_url_shortener.md) | -| DR | [docs/DRs/20100611_3_url_shortener.md](../../docs/DRs/20100611_3_url_shortener.md) | -| Deploy | [DEPLOY.md](DEPLOY.md) | - -**Status:** PHP API implemented (shorten, redirect, QR via `qrencode`). - -**Git remote:** `git://f0xx.org/androicast_project/url-shortener` -**Public URL:** `https://s.f0xx.org` - -## Quick start (BE) - -```bash -mysql -u root -p < sql/schema.mariadb.sql -./scripts/init-mariadb-grants.sh -cp config/config.example.php config/config.php # password = crashes androidcast user -php scripts/seed_bearer.php --label=prod -apk add libqrencode-tools # QR PNG -# sync tree → /var/www/.../apps/s/ -rc-service php-fpm81 restart -export BEARER=… ./scripts/smoke_shorten.sh -``` - -## Tests - -```bash -./scripts/run-php-tests.sh -``` - -## Layout - -```text -backend/url-shortener/ - public/index.php # front controller - src/ # ShortenService, redirect, QR, repos - config/config.example.php - sql/schema.mariadb.sql - openapi.yaml - scripts/seed_bearer.php - scripts/purge_url_shortener.php - scripts/demo_curl.sh - scripts/smoke_shorten.sh -``` diff --git a/backend/url-shortener/config/.gitignore b/backend/url-shortener/config/.gitignore deleted file mode 100644 index 4f4773f..0000000 --- a/backend/url-shortener/config/.gitignore +++ /dev/null @@ -1 +0,0 @@ -config.php diff --git a/backend/url-shortener/config/config.example.php b/backend/url-shortener/config/config.example.php deleted file mode 100644 index db82927..0000000 --- a/backend/url-shortener/config/config.example.php +++ /dev/null @@ -1,20 +0,0 @@ - [ - 'driver' => 'mysql', - 'mysql' => [ - 'host' => '127.0.0.1', - 'port' => 3306, - 'socket' => '/run/mysqld/mysqld.sock', - 'database' => 'url_shortener', - 'username' => 'androidcast', - 'password' => 'change-me-same-as-crashes-config', - 'charset' => 'utf8mb4', - ], - ], - 'public_base' => 'https://s.f0xx.org', -]; diff --git a/backend/url-shortener/config/config.php b/backend/url-shortener/config/config.php deleted file mode 100644 index cd97fbc..0000000 --- a/backend/url-shortener/config/config.php +++ /dev/null @@ -1,20 +0,0 @@ - [ - 'driver' => 'mysql', - 'mysql' => [ - 'host' => '127.0.0.1', - 'port' => 3306, - 'socket' => '/run/mysqld/mysqld.sock', - 'database' => 'url_shortener', - 'username' => 'androidcast', - 'password' => 'androidcastrootowned', - 'charset' => 'utf8mb4', - ], - ], - 'public_base' => 'https://s.f0xx.org', -]; diff --git a/backend/url-shortener/deploy/nginx.be-url-shortener.fragment b/backend/url-shortener/deploy/nginx.be-url-shortener.fragment deleted file mode 100644 index f229d5a..0000000 --- a/backend/url-shortener/deploy/nginx.be-url-shortener.fragment +++ /dev/null @@ -1,29 +0,0 @@ -# BE Alpine — url-shortener vhost fragment -# Include from /etc/nginx/conf.d/apps.conf or separate file. -# -# Docroot: /var/www/localhost/htdocs/apps/s/public -# PHP-FPM: same socket as crash reporter unless load warrants a pool. - -server { - listen 80; - server_name s.f0xx.org; - - root /var/www/localhost/htdocs/apps/s/public; - index index.php; - - # API + QR - location ^~ /api/ { - try_files $uri /index.php?$query_string; - } - - # Short slug redirect (6–16 hex chars). Regex MUST be quoted — bare {6,16} breaks nginx -t. - location ~ "^/[a-f0-9]{6,16}$" { - try_files $uri /index.php?$query_string; - } - - location ~ \.php$ { - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_pass unix:/run/php-fpm.socket; - } -} diff --git a/backend/url-shortener/deploy/nginx.fe-s.f0xx.org.snippet b/backend/url-shortener/deploy/nginx.fe-s.f0xx.org.snippet deleted file mode 100644 index 6283600..0000000 --- a/backend/url-shortener/deploy/nginx.fe-s.f0xx.org.snippet +++ /dev/null @@ -1,48 +0,0 @@ -# FE Gentoo — server block for s.f0xx.org (URL shortener) -# File: tmp/FE_gentoo/etc/nginx/nginx.conf OR conf.d/s.f0xx.org.conf -# -# TLS cert (confirmed on FE): -# ssl_certificate /etc/letsencrypt/live/s.f0xx.org/fullchain.pem; -# ssl_certificate_key /etc/letsencrypt/live/s.f0xx.org/privkey.pem; - -server { - listen 80; - server_name s.f0xx.org; - return 301 https://$host$request_uri; -} - -server { - listen 443 ssl http2; - server_name s.f0xx.org; - - ssl_certificate /etc/letsencrypt/live/s.f0xx.org/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/s.f0xx.org/privkey.pem; - - access_log /var/log/nginx/s.f0xx.org.access_log main; - error_log /var/log/nginx/s.f0xx.org.error_log info; - - ssl_certificate /etc/letsencrypt/live/s.f0xx.org/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/s.f0xx.org/privkey.pem; - - - # All paths → BE :80 (PHP-FPM url-shortener vhost) - location / { - proxy_pass http://artc0.intra.raptor.org:80; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 120s; - proxy_send_timeout 120s; - proxy_read_timeout 180s; - proxy_buffers 16 16m; - proxy_buffer_size 16m; - client_body_buffer_size 16m; - client_max_body_size 16m; - proxy_buffering off; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_cache_bypass $http_upgrade; - } -} diff --git a/backend/url-shortener/openapi.yaml b/backend/url-shortener/openapi.yaml deleted file mode 100644 index d74e822..0000000 --- a/backend/url-shortener/openapi.yaml +++ /dev/null @@ -1,253 +0,0 @@ -openapi: 3.0.3 -info: - title: Android Cast URL Shortener - description: | - Standalone service at https://s.f0xx.org — SPEC docs/specs/20100611_3_url_shortener.md - version: 0.1.0 -servers: - - url: https://s.f0xx.org - description: Production (FE TLS → BE :80) - -paths: - /api/v1/health: - get: - summary: Service health - operationId: health - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - status: - type: string - service: - type: string - - /api/v1/shorten: - get: - summary: Shorten URL (query params) - description: Prefer POST for long URLs or sensitive query strings. - operationId: shortenGet - parameters: - - $ref: '#/components/parameters/url' - - $ref: '#/components/parameters/bearer' - - $ref: '#/components/parameters/signer' - - $ref: '#/components/parameters/ttl' - responses: - '200': - $ref: '#/components/responses/ShortenSuccess' - '400': - $ref: '#/components/responses/ShortenError' - '401': - $ref: '#/components/responses/ShortenError' - '403': - $ref: '#/components/responses/ShortenError' - '409': - $ref: '#/components/responses/ShortenError' - '429': - $ref: '#/components/responses/ShortenError' - post: - summary: Shorten URL (JSON body) — preferred - operationId: shortenPost - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ShortenRequest' - responses: - '200': - $ref: '#/components/responses/ShortenSuccess' - '400': - $ref: '#/components/responses/ShortenError' - '401': - $ref: '#/components/responses/ShortenError' - '403': - $ref: '#/components/responses/ShortenError' - '409': - $ref: '#/components/responses/ShortenError' - '429': - $ref: '#/components/responses/ShortenError' - - /api/v1/qr/{slug}.png: - get: - summary: QR code PNG for short URL - operationId: qrPng - parameters: - - name: slug - in: path - required: true - schema: - type: string - pattern: '^[a-f0-9]{6,16}$' - - name: size - in: query - schema: - type: integer - default: 256 - minimum: 128 - maximum: 512 - responses: - '200': - description: PNG image - content: - image/png: - schema: - type: string - format: binary - '404': - description: Unknown slug - '410': - description: Expired slug - - /{slug}: - get: - summary: Redirect to original URL - operationId: redirect - parameters: - - name: slug - in: path - required: true - schema: - type: string - pattern: '^[a-f0-9]{6,16}$' - responses: - '302': - description: Found — Location is original URL - headers: - Location: - schema: - type: string - format: uri - '404': - description: Unknown slug - '410': - description: Expired slug - head: - summary: Redirect probe (no body) - operationId: redirectHead - parameters: - - name: slug - in: path - required: true - schema: - type: string - responses: - '302': - description: Found - '404': - description: Unknown - '410': - description: Expired - -components: - parameters: - url: - name: url - in: query - required: true - schema: - type: string - format: uri - maxLength: 2048 - bearer: - name: bearer - in: query - required: true - schema: - type: string - minLength: 8 - signer: - name: signer - in: query - required: false - schema: - type: string - ttl: - name: ttl - in: query - required: false - description: Seconds until expiry; 0 = permanent (signer required) - schema: - type: integer - minimum: 0 - default: 86400 - - schemas: - ShortenRequest: - type: object - required: [url, bearer] - properties: - url: - type: string - format: uri - maxLength: 2048 - bearer: - type: string - signer: - type: string - ttl: - type: integer - minimum: 0 - default: 86400 - ShortenSuccess: - type: object - required: [code, url, u, bearer, ttl] - properties: - code: - type: string - enum: ['0'] - url: - type: string - format: uri - u: - type: string - format: uri - description: Short URL - bearer: - type: string - ttl: - type: string - description: Remaining seconds as string; "0" if permanent - qr: - type: string - format: uri - description: Optional v1.1 — URL to QR PNG - ShortenError: - type: object - required: [code, desc] - properties: - code: - type: string - enum: - - ACCESS_DENIED - - MISSING_BEARER - - INVALID_URL - - TTL_EXPIRED - - SIGNER_REQUIRED - - RATE_LIMITED - - INTERNAL - - NOT_IMPLEMENTED - url: - type: string - bearer: - type: string - desc: - type: string - - responses: - ShortenSuccess: - description: Short URL created or cached - content: - application/json: - schema: - $ref: '#/components/schemas/ShortenSuccess' - ShortenError: - description: Error - content: - application/json: - schema: - $ref: '#/components/schemas/ShortenError' diff --git a/backend/url-shortener/public/index.php b/backend/url-shortener/public/index.php deleted file mode 100644 index 863e42c..0000000 --- a/backend/url-shortener/public/index.php +++ /dev/null @@ -1,72 +0,0 @@ -query('SELECT 1'); - JsonResponse::ok([ - 'status' => 'ok', - 'service' => 'url-shortener', - 'db' => 'connected', - ]); - } - - if ($uri === '/api/v1/shorten') { - if ($method === 'GET') { - $result = ShortenService::handle( - (string) ($_GET['url'] ?? ''), - (string) ($_GET['bearer'] ?? ''), - (string) ($_GET['signer'] ?? ''), - isset($_GET['ttl']) ? (int) $_GET['ttl'] : null - ); - } elseif ($method === 'POST') { - $raw = file_get_contents('php://input') ?: ''; - $body = json_decode($raw, true); - if (!is_array($body)) { - JsonResponse::error(400, 'INVALID_URL', 'expected JSON body'); - } - $result = ShortenService::handle( - (string) ($body['url'] ?? ''), - (string) ($body['bearer'] ?? ''), - (string) ($body['signer'] ?? ''), - isset($body['ttl']) ? (int) $body['ttl'] : null - ); - } else { - JsonResponse::error(405, 'INTERNAL', 'method not allowed'); - } - $http = (int) ($result['http'] ?? 200); - unset($result['http']); - if (($result['code'] ?? '') === '0') { - JsonResponse::ok($result, $http); - } - JsonResponse::error($http, (string) $result['code'], (string) $result['desc'], [ - 'url' => $result['url'] ?? '', - 'bearer' => $result['bearer'] ?? '', - ]); - } - - if (preg_match('#^/api/v1/qr/([a-f0-9]{6,16})\.png$#', $uri, $m)) { - $size = isset($_GET['size']) ? (int) $_GET['size'] : 256; - QrHandler::handle($m[1], $size); - } - - if (preg_match('#^/([a-f0-9]{6,16})$#', $uri, $m)) { - if ($method === 'GET' || $method === 'HEAD') { - RedirectHandler::handle($m[1], $method); - } - JsonResponse::error(405, 'INTERNAL', 'method not allowed'); - } - - http_response_code(404); - header('Content-Type: application/json; charset=utf-8'); - echo json_encode(['code' => 'NOT_FOUND', 'desc' => 'not found'], JSON_UNESCAPED_SLASHES); -} catch (Throwable $e) { - error_log('url-shortener: ' . $e->getMessage()); - JsonResponse::error(500, 'INTERNAL', 'server error'); -} diff --git a/backend/url-shortener/scripts/demo_curl.sh b/backend/url-shortener/scripts/demo_curl.sh deleted file mode 100755 index d30d121..0000000 --- a/backend/url-shortener/scripts/demo_curl.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -# End-to-end curl cookbook — SPEC appendix A. -# Usage: -# export BASE=https://s.f0xx.org -# export BEARER=your-trusted-token -# export SIGNER=optional-for-ttl0 -# ./scripts/demo_curl.sh -set -euo pipefail - -BASE="${BASE:-https://s.f0xx.org}" -BEARER="${BEARER:?set BEARER to a trusted API token}" -SIGNER="${SIGNER:-}" -TARGET="${TARGET:-https://defender.net/very/long/link?demo=1}" -TTL="${TTL:-3600}" - -echo "==> 1) Health" -curl -fsS "$BASE/api/v1/health" | head -c 200 -echo - -echo "==> 2) Shorten (POST preferred)" -SHORT_JSON="$(curl -fsS -X POST "$BASE/api/v1/shorten" \ - -H 'Content-Type: application/json' \ - -d "$(jq -nc --arg url "$TARGET" --arg bearer "$BEARER" --argjson ttl "$TTL" \ - '{url:$url,bearer:$bearer,ttl:$ttl}')")" -echo "$SHORT_JSON" | jq . - -SHORT="$(echo "$SHORT_JSON" | jq -r '.u // empty')" -if [[ -z "$SHORT" ]]; then - echo "shorten failed (no .u in response)" >&2 - exit 1 -fi -SLUG="${SHORT##*/}" - -echo "==> 3) Shorten cache hit (same bearer + url)" -curl -fsS -X POST "$BASE/api/v1/shorten" \ - -H 'Content-Type: application/json' \ - -d "$(jq -nc --arg url "$TARGET" --arg bearer "$BEARER" --argjson ttl "$TTL" \ - '{url:$url,bearer:$bearer,ttl:$ttl}')" | jq . - -echo "==> 4) Redirect (HEAD — no body)" -curl -sSI "$SHORT" | grep -E '^(HTTP|Location:)' - -echo "==> 5) QR PNG" -curl -fsS -o "/tmp/url-shortener-qr-${SLUG}.png" "$BASE/api/v1/qr/${SLUG}.png?size=256" -file "/tmp/url-shortener-qr-${SLUG}.png" - -if [[ -n "$SIGNER" ]]; then - echo "==> 6) Permanent link (ttl=0, signer required)" - curl -fsS -X POST "$BASE/api/v1/shorten" \ - -H 'Content-Type: application/json' \ - -d "$(jq -nc --arg url "$TARGET&perm=1" --arg bearer "$BEARER" --arg signer "$SIGNER" \ - '{url:$url,bearer:$bearer,signer:$signer,ttl:0}')" | jq . -fi - -echo "==> Done" diff --git a/backend/url-shortener/scripts/init-mariadb-grants.sh b/backend/url-shortener/scripts/init-mariadb-grants.sh deleted file mode 100755 index 87a8124..0000000 --- a/backend/url-shortener/scripts/init-mariadb-grants.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -# Grant existing androidcast app user access to url_shortener (after schema import as root). -# Password is NOT stored here — same as examples/crash_reporter/backend/config/config.php db.mysql.password -# -# Usage (on BE): -# mysql -u root -p < sql/schema.mariadb.sql -# ./scripts/init-mariadb-grants.sh -set -eu - -DB_NAME="${URL_SHORTENER_DB_NAME:-url_shortener}" -DB_USER="${URL_SHORTENER_DB_USER:-androidcast}" - -echo "Granting ${DB_USER}@localhost and @127.0.0.1 on ${DB_NAME}.* ..." -mysql -u root -p <exec( - 'DELETE FROM links WHERE expires_at IS NOT NULL AND expires_at < NOW()' -); -$deletedAudit = $pdo->exec( - 'DELETE FROM audit_log WHERE created_at < (NOW() - INTERVAL 90 DAY)' -); - -echo "purge: links=$deletedLinks audit=$deletedAudit\n"; diff --git a/backend/url-shortener/scripts/run-php-tests.sh b/backend/url-shortener/scripts/run-php-tests.sh deleted file mode 100755 index dfe4ab1..0000000 --- a/backend/url-shortener/scripts/run-php-tests.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -# PHP unit-style checks (no phpunit required). -set -euo pipefail -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -php "$ROOT/tests/run_tests.php" diff --git a/backend/url-shortener/scripts/seed_bearer.php b/backend/url-shortener/scripts/seed_bearer.php deleted file mode 100755 index 0ddb129..0000000 --- a/backend/url-shortener/scripts/seed_bearer.php +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env php - health $BASE/api/v1/health" -body="$(curl -fsS "$BASE/api/v1/health")" -echo "$body" | grep -q '"status":"ok"' || { echo "health not ok: $body"; exit 1; } - -if [[ -z "$BEARER" ]]; then - echo "SKIP shorten (set BEARER to run full smoke)" - exit 0 -fi - -echo "==> shorten POST" -resp="$(curl -fsS -X POST "$BASE/api/v1/shorten" \ - -H 'Content-Type: application/json' \ - -d '{"url":"https://example.com/smoke-'"$(date +%s)"'","bearer":"'"$BEARER"'","ttl":300}')" -echo "$resp" -short="$(echo "$resp" | sed -n 's/.*"u":"\([^"]*\)".*/\1/p')" -[[ -n "$short" ]] || { echo "no short url in response"; exit 1; } - -echo "==> redirect HEAD" -curl -fsSI "$short" | grep -qi '^location:' || { echo "missing redirect"; exit 1; } - -echo "OK" diff --git a/backend/url-shortener/sql/schema.mariadb.sql b/backend/url-shortener/sql/schema.mariadb.sql deleted file mode 100644 index 39fa560..0000000 --- a/backend/url-shortener/sql/schema.mariadb.sql +++ /dev/null @@ -1,62 +0,0 @@ --- URL shortener — MariaDB schema (SPEC docs/specs/20100611_3_url_shortener.md §9) -CREATE DATABASE IF NOT EXISTS url_shortener - CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -USE url_shortener; - -CREATE TABLE IF NOT EXISTS bearer_tokens ( - id INT UNSIGNED NOT NULL AUTO_INCREMENT, - token_hash CHAR(64) NOT NULL, - label VARCHAR(128) NOT NULL DEFAULT '', - trusted TINYINT(1) NOT NULL DEFAULT 0, - can_temporary TINYINT(1) NOT NULL DEFAULT 1, - can_permanent TINYINT(1) NOT NULL DEFAULT 0, - rate_limit_per_hour INT UNSIGNED NOT NULL DEFAULT 1000, - revoked_at DATETIME NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - UNIQUE KEY uq_bearer_token_hash (token_hash) -) ENGINE=InnoDB; - -CREATE TABLE IF NOT EXISTS signer_keys ( - id INT UNSIGNED NOT NULL AUTO_INCREMENT, - key_hash CHAR(64) NOT NULL, - label VARCHAR(128) NOT NULL DEFAULT '', - can_permanent TINYINT(1) NOT NULL DEFAULT 1, - revoked_at DATETIME NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - UNIQUE KEY uq_signer_key_hash (key_hash) -) ENGINE=InnoDB; - -CREATE TABLE IF NOT EXISTS links ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - bearer_id INT UNSIGNED NOT NULL, - signer_id INT UNSIGNED NULL, - slug VARCHAR(16) NOT NULL, - origin TEXT NOT NULL, - origin_hash CHAR(64) NOT NULL, - ttl_seconds INT UNSIGNED NOT NULL DEFAULT 86400, - expires_at DATETIME NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - accessed_at DATETIME NULL, - access_count BIGINT UNSIGNED NOT NULL DEFAULT 0, - PRIMARY KEY (id), - UNIQUE KEY uq_slug (slug), - KEY idx_bearer_origin (bearer_id, origin_hash), - CONSTRAINT fk_links_bearer FOREIGN KEY (bearer_id) REFERENCES bearer_tokens (id), - CONSTRAINT fk_links_signer FOREIGN KEY (signer_id) REFERENCES signer_keys (id) -) ENGINE=InnoDB; - -CREATE TABLE IF NOT EXISTS audit_log ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - event VARCHAR(32) NOT NULL, - bearer_id INT UNSIGNED NULL, - slug VARCHAR(16) NULL, - ip VARCHAR(45) NOT NULL DEFAULT '', - detail TEXT, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_audit_created (created_at) -) ENGINE=InnoDB; diff --git a/backend/url-shortener/src/AuditLog.php b/backend/url-shortener/src/AuditLog.php deleted file mode 100644 index f6357ce..0000000 --- a/backend/url-shortener/src/AuditLog.php +++ /dev/null @@ -1,18 +0,0 @@ -prepare( - 'INSERT INTO audit_log (event, bearer_id, slug, ip, detail) VALUES (?, ?, ?, ?, ?)' - ); - $stmt->execute([$event, $bearerId, $slug, $ip, $detail]); - } -} diff --git a/backend/url-shortener/src/BearerRepository.php b/backend/url-shortener/src/BearerRepository.php deleted file mode 100644 index d269906..0000000 --- a/backend/url-shortener/src/BearerRepository.php +++ /dev/null @@ -1,68 +0,0 @@ -|null */ - public static function findByToken(string $token): ?array { - $pdo = Database::pdo(); - $stmt = $pdo->prepare( - 'SELECT * FROM bearer_tokens WHERE token_hash = ? AND revoked_at IS NULL LIMIT 1' - ); - $stmt->execute([self::hashToken($token)]); - $row = $stmt->fetch(); - return $row === false ? null : $row; - } - - public static function isTrusted(array $bearer): bool { - return (int) ($bearer['trusted'] ?? 0) === 1; - } - - public static function canTemporary(array $bearer): bool { - return (int) ($bearer['can_temporary'] ?? 0) === 1; - } - - public static function canPermanent(array $bearer): bool { - return (int) ($bearer['can_permanent'] ?? 0) === 1; - } - - public static function rateLimitPerHour(array $bearer): int { - return max(1, (int) ($bearer['rate_limit_per_hour'] ?? 1000)); - } - - public static function countCreatesLastHour(int $bearerId): int { - $pdo = Database::pdo(); - $stmt = $pdo->prepare( - 'SELECT COUNT(*) AS c FROM links WHERE bearer_id = ? AND created_at >= (NOW() - INTERVAL 1 HOUR)' - ); - $stmt->execute([$bearerId]); - $row = $stmt->fetch(); - return (int) ($row['c'] ?? 0); - } - - /** @return array{token: string, id: int} */ - public static function createTrusted( - string $label, - bool $canTemporary = true, - bool $canPermanent = false, - int $rateLimit = 1000 - ): array { - $token = bin2hex(random_bytes(24)); - $pdo = Database::pdo(); - $stmt = $pdo->prepare( - 'INSERT INTO bearer_tokens (token_hash, label, trusted, can_temporary, can_permanent, rate_limit_per_hour) - VALUES (?, ?, 1, ?, ?, ?)' - ); - $stmt->execute([ - self::hashToken($token), - $label, - $canTemporary ? 1 : 0, - $canPermanent ? 1 : 0, - $rateLimit, - ]); - return ['token' => $token, 'id' => (int) $pdo->lastInsertId()]; - } -} diff --git a/backend/url-shortener/src/Database.php b/backend/url-shortener/src/Database.php deleted file mode 100644 index b431047..0000000 --- a/backend/url-shortener/src/Database.php +++ /dev/null @@ -1,32 +0,0 @@ - PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - ]); - return self::$pdo; - } -} diff --git a/backend/url-shortener/src/JsonResponse.php b/backend/url-shortener/src/JsonResponse.php deleted file mode 100644 index 40f68f8..0000000 --- a/backend/url-shortener/src/JsonResponse.php +++ /dev/null @@ -1,23 +0,0 @@ - $extra */ - public static function error(int $http, string $code, string $desc, array $extra = []): void { - http_response_code($http); - header('Content-Type: application/json; charset=utf-8'); - echo json_encode(array_merge([ - 'code' => $code, - 'desc' => $desc, - ], $extra), JSON_UNESCAPED_SLASHES); - exit; - } - - /** @param array $body */ - public static function ok(array $body, int $http = 200): void { - http_response_code($http); - header('Content-Type: application/json; charset=utf-8'); - echo json_encode($body, JSON_UNESCAPED_SLASHES); - exit; - } -} diff --git a/backend/url-shortener/src/LinkRepository.php b/backend/url-shortener/src/LinkRepository.php deleted file mode 100644 index 9507507..0000000 --- a/backend/url-shortener/src/LinkRepository.php +++ /dev/null @@ -1,109 +0,0 @@ -|null */ - public static function findActiveByBearerOrigin(int $bearerId, string $originHash): ?array { - $pdo = Database::pdo(); - $stmt = $pdo->prepare( - 'SELECT * FROM links WHERE bearer_id = ? AND origin_hash = ? - ORDER BY id DESC LIMIT 1' - ); - $stmt->execute([$bearerId, $originHash]); - $row = $stmt->fetch(); - if ($row === false) { - return null; - } - return self::isExpired($row) ? null : $row; - } - - /** @return array|null */ - public static function findExpiredByBearerOrigin(int $bearerId, string $originHash): ?array { - $pdo = Database::pdo(); - $stmt = $pdo->prepare( - 'SELECT * FROM links WHERE bearer_id = ? AND origin_hash = ? - ORDER BY id DESC LIMIT 1' - ); - $stmt->execute([$bearerId, $originHash]); - $row = $stmt->fetch(); - if ($row === false) { - return null; - } - return self::isExpired($row) ? $row : null; - } - - /** @param array $row */ - public static function isExpired(array $row): bool { - $exp = $row['expires_at'] ?? null; - if ($exp === null || $exp === '') { - return false; - } - return strtotime((string) $exp) <= time(); - } - - /** @param array $row */ - public static function remainingTtlSeconds(array $row): string { - $exp = $row['expires_at'] ?? null; - if ($exp === null || $exp === '') { - return '0'; - } - $rem = max(0, strtotime((string) $exp) - time()); - return (string) $rem; - } - - /** @return array|null */ - public static function findBySlug(string $slug): ?array { - $pdo = Database::pdo(); - $stmt = $pdo->prepare('SELECT * FROM links WHERE slug = ? LIMIT 1'); - $stmt->execute([$slug]); - $row = $stmt->fetch(); - return $row === false ? null : $row; - } - - public static function slugExists(string $slug): bool { - return self::findBySlug($slug) !== null; - } - - public static function insert( - int $bearerId, - ?int $signerId, - string $slug, - string $origin, - string $originHash, - int $ttlSeconds - ): void { - $pdo = Database::pdo(); - $expiresAt = $ttlSeconds <= 0 - ? null - : date('Y-m-d H:i:s', time() + $ttlSeconds); - $stmt = $pdo->prepare( - 'INSERT INTO links (bearer_id, signer_id, slug, origin, origin_hash, ttl_seconds, expires_at) - VALUES (?, ?, ?, ?, ?, ?, ?)' - ); - $stmt->execute([ - $bearerId, - $signerId, - $slug, - $origin, - $originHash, - max(0, $ttlSeconds), - $expiresAt, - ]); - } - - public static function recordRedirect(string $slug): void { - $pdo = Database::pdo(); - $stmt = $pdo->prepare( - 'UPDATE links SET accessed_at = NOW(), access_count = access_count + 1 WHERE slug = ?' - ); - $stmt->execute([$slug]); - } -} diff --git a/backend/url-shortener/src/QrHandler.php b/backend/url-shortener/src/QrHandler.php deleted file mode 100644 index cf59f46..0000000 --- a/backend/url-shortener/src/QrHandler.php +++ /dev/null @@ -1,50 +0,0 @@ - 'NOT_FOUND', 'desc' => 'unknown slug'], JSON_UNESCAPED_SLASHES); - exit; - } - if (LinkRepository::isExpired($link)) { - http_response_code(410); - header('Content-Type: application/json; charset=utf-8'); - echo json_encode(['code' => 'TTL_EXPIRED', 'desc' => 'link expired'], JSON_UNESCAPED_SLASHES); - exit; - } - - $size = max(128, min(512, $size)); - $target = LinkRepository::shortUrl($slug); - $png = self::renderPng($target, $size); - if ($png === null) { - JsonResponse::error(503, 'INTERNAL', 'QR generation unavailable (install qrencode on BE)'); - } - header('Content-Type: image/png'); - header('Cache-Control: public, max-age=3600'); - echo $png; - exit; - } - - private static function renderPng(string $text, int $size): ?string { - $qrencode = trim((string) shell_exec('command -v qrencode 2>/dev/null') ?? ''); - if ($qrencode === '') { - return null; - } - $moduleSize = max(2, (int) round($size / 40)); - $cmd = sprintf( - '%s -t PNG -s %d -m 1 -o - %s 2>/dev/null', - escapeshellcmd($qrencode), - $moduleSize, - escapeshellarg($text) - ); - $out = shell_exec($cmd); - if (!is_string($out) || strlen($out) < 8) { - return null; - } - return $out; - } -} diff --git a/backend/url-shortener/src/RedirectHandler.php b/backend/url-shortener/src/RedirectHandler.php deleted file mode 100644 index 0ce9163..0000000 --- a/backend/url-shortener/src/RedirectHandler.php +++ /dev/null @@ -1,48 +0,0 @@ - 'NOT_FOUND', 'desc' => 'unknown slug'], JSON_UNESCAPED_SLASHES); - } else { - header('Content-Type: text/plain; charset=utf-8'); - echo 'Not found'; - } - exit; - } - - private static function respondExpired(string $method): void { - http_response_code(410); - if (self::wantsJson()) { - header('Content-Type: application/json; charset=utf-8'); - echo json_encode(['code' => 'TTL_EXPIRED', 'desc' => 'link expired'], JSON_UNESCAPED_SLASHES); - } else { - header('Content-Type: text/plain; charset=utf-8'); - echo 'Gone'; - } - exit; - } - - private static function wantsJson(): bool { - $accept = $_SERVER['HTTP_ACCEPT'] ?? ''; - return str_contains($accept, 'application/json'); - } -} diff --git a/backend/url-shortener/src/ShortenService.php b/backend/url-shortener/src/ShortenService.php deleted file mode 100644 index a50a49e..0000000 --- a/backend/url-shortener/src/ShortenService.php +++ /dev/null @@ -1,113 +0,0 @@ - */ - public static function handle(string $rawUrl, string $bearerToken, string $signerKey, ?int $ttl): array { - $bearerToken = trim($bearerToken); - if ($bearerToken === '') { - return self::err(401, 'MISSING_BEARER', 'missing bearer', $rawUrl, ''); - } - - $bearer = BearerRepository::findByToken($bearerToken); - if ($bearer === null) { - AuditLog::write('deny', null, null, 'unknown bearer'); - return self::err(403, 'ACCESS_DENIED', 'access denied', $rawUrl, $bearerToken); - } - $bearerId = (int) $bearer['id']; - - try { - $normalized = UrlNormalizer::normalize($rawUrl); - } catch (InvalidArgumentException $e) { - return self::err(400, 'INVALID_URL', $e->getMessage(), $rawUrl, $bearerToken); - } - $originHash = UrlNormalizer::originHash($normalized); - - $cached = LinkRepository::findActiveByBearerOrigin($bearerId, $originHash); - if ($cached !== null) { - return self::success($normalized, $cached, $bearerToken); - } - - $expired = LinkRepository::findExpiredByBearerOrigin($bearerId, $originHash); - if ($expired !== null) { - return self::err(409, 'TTL_EXPIRED', 'link expired', $normalized, $bearerToken); - } - - if (!BearerRepository::isTrusted($bearer)) { - AuditLog::write('deny', $bearerId, null, 'untrusted bearer'); - return self::err(403, 'ACCESS_DENIED', 'access denied', $normalized, $bearerToken); - } - - $ttlSeconds = $ttl ?? 86400; - if ($ttlSeconds < 0) { - $ttlSeconds = 86400; - } - - $signerId = null; - if ($ttlSeconds === 0) { - $signer = SignerRepository::findValid($signerKey); - if ($signer === null) { - return self::err(403, 'SIGNER_REQUIRED', 'signer required for permanent links', $normalized, $bearerToken); - } - if (!BearerRepository::canPermanent($bearer)) { - return self::err(403, 'ACCESS_DENIED', 'bearer cannot create permanent links', $normalized, $bearerToken); - } - $signerId = (int) $signer['id']; - } else { - if (!BearerRepository::canTemporary($bearer)) { - return self::err(403, 'ACCESS_DENIED', 'bearer cannot create temporary links', $normalized, $bearerToken); - } - } - - $limit = BearerRepository::rateLimitPerHour($bearer); - if (BearerRepository::countCreatesLastHour($bearerId) >= $limit) { - return self::err(429, 'RATE_LIMITED', 'rate limit exceeded', $normalized, $bearerToken); - } - - $slug = self::allocateSlug($bearerId, $normalized); - LinkRepository::insert($bearerId, $signerId, $slug, $normalized, $originHash, $ttlSeconds); - AuditLog::write('shorten', $bearerId, $slug, 'created'); - - $row = LinkRepository::findBySlug($slug); - if ($row === null) { - return self::err(500, 'INTERNAL', 'insert failed', $normalized, $bearerToken); - } - return self::success($normalized, $row, $bearerToken); - } - - private static function allocateSlug(int $bearerId, string $normalized): string { - foreach (SlugGenerator::candidates($bearerId, $normalized) as $candidate) { - if (!LinkRepository::slugExists($candidate)) { - return $candidate; - } - AuditLog::write('warn', $bearerId, $candidate, 'slug collision'); - } - throw new RuntimeException('slug exhaustion'); - } - - /** @param array $row */ - private static function success(string $normalized, array $row, string $bearerToken): array { - $slug = (string) $row['slug']; - $body = [ - 'http' => 200, - 'code' => '0', - 'url' => $normalized, - 'u' => LinkRepository::shortUrl($slug), - 'bearer' => $bearerToken, - 'ttl' => LinkRepository::remainingTtlSeconds($row), - 'qr' => LinkRepository::publicBase() . '/api/v1/qr/' . $slug . '.png', - ]; - return $body; - } - - /** @return array */ - private static function err(int $http, string $code, string $desc, string $url, string $bearer): array { - return [ - 'http' => $http, - 'code' => $code, - 'url' => $url, - 'bearer' => $bearer, - 'desc' => $desc, - ]; - } -} diff --git a/backend/url-shortener/src/SignerRepository.php b/backend/url-shortener/src/SignerRepository.php deleted file mode 100644 index b00fea9..0000000 --- a/backend/url-shortener/src/SignerRepository.php +++ /dev/null @@ -1,33 +0,0 @@ -|null */ - public static function findValid(string $signerKey): ?array { - if ($signerKey === '') { - return null; - } - $pdo = Database::pdo(); - $stmt = $pdo->prepare( - 'SELECT * FROM signer_keys WHERE key_hash = ? AND revoked_at IS NULL AND can_permanent = 1 LIMIT 1' - ); - $stmt->execute([self::hashKey($signerKey)]); - $row = $stmt->fetch(); - return $row === false ? null : $row; - } - - /** @return array{key: string, id: int} */ - public static function create(string $label): array { - $key = bin2hex(random_bytes(24)); - $pdo = Database::pdo(); - $stmt = $pdo->prepare( - 'INSERT INTO signer_keys (key_hash, label, can_permanent) VALUES (?, ?, 1)' - ); - $stmt->execute([self::hashKey($key), $label]); - return ['key' => $key, 'id' => (int) $pdo->lastInsertId()]; - } -} diff --git a/backend/url-shortener/src/SlugGenerator.php b/backend/url-shortener/src/SlugGenerator.php deleted file mode 100644 index 772c640..0000000 --- a/backend/url-shortener/src/SlugGenerator.php +++ /dev/null @@ -1,19 +0,0 @@ - suffix candidates on collision */ - public static function candidates(int $bearerId, string $normalizedUrl): array { - $slugs = [self::baseSlug($bearerId, $normalizedUrl)]; - $base = $bearerId . "\x1f" . $normalizedUrl; - for ($i = 1; $i <= 15; $i++) { - $slugs[] = substr(hash('sha256', $base . "\x1e" . $i), 0, 12); - } - return $slugs; - } -} diff --git a/backend/url-shortener/src/UrlNormalizer.php b/backend/url-shortener/src/UrlNormalizer.php deleted file mode 100644 index e08e644..0000000 --- a/backend/url-shortener/src/UrlNormalizer.php +++ /dev/null @@ -1,60 +0,0 @@ - self::MAX_LEN) { - throw new InvalidArgumentException('url too long'); - } - $parts = parse_url($url); - if ($parts === false || !isset($parts['scheme'], $parts['host'])) { - throw new InvalidArgumentException('malformed url'); - } - $scheme = strtolower((string) $parts['scheme']); - if ($scheme !== 'http' && $scheme !== 'https') { - throw new InvalidArgumentException('unsupported scheme'); - } - $host = strtolower((string) $parts['host']); - self::assertHostAllowed($host); - - $port = isset($parts['port']) ? ':' . (int) $parts['port'] : ''; - $path = $parts['path'] ?? ''; - $query = isset($parts['query']) ? '?' . $parts['query'] : ''; - $fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : ''; - - return $scheme . '://' . $host . $port . $path . $query . $fragment; - } - - public static function originHash(string $normalized): string { - return hash('sha256', $normalized); - } - - private static function assertHostAllowed(string $host): void { - if ($host === 'localhost' || str_ends_with($host, '.local') || str_ends_with($host, '.internal')) { - throw new InvalidArgumentException('blocked host'); - } - if ($host === 'metadata.google.internal' || $host === 'metadata') { - throw new InvalidArgumentException('blocked host'); - } - if (filter_var($host, FILTER_VALIDATE_IP)) { - self::assertIpAllowed($host); - return; - } - $resolved = @gethostbyname($host); - if ($resolved !== $host && filter_var($resolved, FILTER_VALIDATE_IP)) { - self::assertIpAllowed($resolved); - } - } - - private static function assertIpAllowed(string $ip): void { - if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { - throw new InvalidArgumentException('blocked host'); - } - } -} diff --git a/backend/url-shortener/src/bootstrap.php b/backend/url-shortener/src/bootstrap.php deleted file mode 100644 index 17b7d4c..0000000 --- a/backend/url-shortener/src/bootstrap.php +++ /dev/null @@ -1,34 +0,0 @@ - $config */ -$config = require $configPath; - -function cfg(string $key, $default = null) { - global $config; - $parts = explode('.', $key); - $v = $config; - foreach ($parts as $p) { - if (!is_array($v) || !array_key_exists($p, $v)) { - return $default; - } - $v = $v[$p]; - } - return $v; -} - -require_once __DIR__ . '/Database.php'; -require_once __DIR__ . '/JsonResponse.php'; -require_once __DIR__ . '/UrlNormalizer.php'; -require_once __DIR__ . '/SlugGenerator.php'; -require_once __DIR__ . '/AuditLog.php'; -require_once __DIR__ . '/BearerRepository.php'; -require_once __DIR__ . '/SignerRepository.php'; -require_once __DIR__ . '/LinkRepository.php'; -require_once __DIR__ . '/ShortenService.php'; -require_once __DIR__ . '/RedirectHandler.php'; -require_once __DIR__ . '/QrHandler.php'; diff --git a/backend/url-shortener/tests/run_tests.php b/backend/url-shortener/tests/run_tests.php deleted file mode 100644 index 2733aaf..0000000 --- a/backend/url-shortener/tests/run_tests.php +++ /dev/null @@ -1,35 +0,0 @@ -= 2, 'collision candidates'); - -echo "OK url-shortener php tests\n";