Initial commit
This commit is contained in:
115
api/contact.php
Normal file
115
api/contact.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../includes/bootstrap.php';
|
||||
|
||||
// ensure session csrf token exists (parity with frontend)
|
||||
if (empty($_SESSION['_csrf']) && !empty($_SESSION['csrf'])) {
|
||||
$_SESSION['_csrf'] = $_SESSION['csrf'];
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
function json_out(array $payload, int $code = 200): void {
|
||||
http_response_code($code);
|
||||
echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
json_out(['ok' => false, 'error' => 'method_not_allowed'], 405);
|
||||
}
|
||||
|
||||
$csrf = (string)($_POST['csrf'] ?? '');
|
||||
if (!csrf_check($csrf)) {
|
||||
json_out(['ok' => false, 'error' => 'Invalid session, refresh and try again.'], 400);
|
||||
}
|
||||
|
||||
// Honeypot to deter bots
|
||||
if (!empty($_POST['website'])) {
|
||||
json_out(['ok' => true]);
|
||||
}
|
||||
|
||||
$last = (int)($_SESSION['contact_last'] ?? 0);
|
||||
if ($last && (time() - $last) < 20) {
|
||||
json_out(['ok' => false, 'error' => 'Please wait a moment before sending again.'], 429);
|
||||
}
|
||||
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$email = trim((string)($_POST['email'] ?? ''));
|
||||
$message = trim((string)($_POST['message'] ?? ''));
|
||||
|
||||
if ($name === '' || mb_strlen($name) > 80) {
|
||||
json_out(['ok' => false, 'error' => 'Invalid name.'], 400);
|
||||
}
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email) > 120) {
|
||||
json_out(['ok' => false, 'error' => 'Invalid email.'], 400);
|
||||
}
|
||||
if ($message === '' || mb_strlen($message) > 2000) {
|
||||
json_out(['ok' => false, 'error' => 'Invalid message length.'], 400);
|
||||
}
|
||||
|
||||
$name = str_replace(["\r", "\n"], ' ', $name);
|
||||
$email = str_replace(["\r", "\n"], '', $email);
|
||||
|
||||
// Ensure table exists
|
||||
try {
|
||||
pdo()->exec("
|
||||
CREATE TABLE IF NOT EXISTS contact_requests (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(80) NOT NULL,
|
||||
email VARCHAR(120) NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
status ENUM('new','read','archived') NOT NULL DEFAULT 'new',
|
||||
created_at DATETIME NOT NULL,
|
||||
ip VARCHAR(45) NULL,
|
||||
user_agent VARCHAR(255) NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
");
|
||||
} catch (Throwable $e) {
|
||||
json_out(['ok' => false, 'error' => 'Server error (contact table).'], 500);
|
||||
}
|
||||
|
||||
$ipRaw = (string)($_SERVER['REMOTE_ADDR'] ?? '');
|
||||
$ip = substr($ipRaw !== '' ? $ipRaw : '127.0.0.1', 0, 45);
|
||||
$ua = substr((string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$id = null;
|
||||
|
||||
try {
|
||||
$st = pdo()->prepare("INSERT INTO contact_requests (name, email, message, status, created_at, ip, user_agent)
|
||||
VALUES (?, ?, ?, 'new', ?, ?, ?)");
|
||||
$st->execute([$name, $email, $message, $now, $ip, $ua]);
|
||||
$id = (int)pdo()->lastInsertId();
|
||||
} catch (Throwable $e) {
|
||||
json_out(['ok' => false, 'error' => 'Server error (save failed).'], 500);
|
||||
}
|
||||
|
||||
// Optional email notification if configured
|
||||
$delivered = false;
|
||||
$to = getenv('CONTACT_TO') ?: '';
|
||||
if ($to !== '') {
|
||||
$subject = "Portfolio contact from {$name}";
|
||||
$body =
|
||||
"Name: {$name}\n" .
|
||||
"Email: {$email}\n\n" .
|
||||
"Message:\n{$message}\n";
|
||||
|
||||
$headers = [
|
||||
'From: Portfolio <no-reply@' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '>',
|
||||
'Reply-To: ' . $email,
|
||||
'Content-Type: text/plain; charset=utf-8'
|
||||
];
|
||||
|
||||
$delivered = @mail($to, $subject, $body, implode("\r\n", $headers)) === true;
|
||||
}
|
||||
|
||||
$_SESSION['contact_last'] = time();
|
||||
|
||||
json_out([
|
||||
'ok' => true,
|
||||
'id' => $id,
|
||||
'delivered' => $delivered,
|
||||
'ip' => $ip,
|
||||
]);
|
||||
179
api/spotify.php
Normal file
179
api/spotify.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// KEEP YOUR PATH - if this file is /api/spotify.php in project root, this is correct.
|
||||
// If your file is /api/spotify.php then change to: require __DIR__ . '/../../includes/bootstrap.php';
|
||||
require __DIR__ . '/../includes/bootstrap.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
function json_out(array $a): void {
|
||||
echo json_encode($a, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
function safe_err(string $code, array $extra = []): void {
|
||||
json_out(array_merge(['ok'=>false, 'error'=>$code], $extra));
|
||||
}
|
||||
|
||||
function spotify_refresh_access_token(string $refreshToken): array {
|
||||
if (!defined('SPOTIFY_CLIENT_ID') || !defined('SPOTIFY_CLIENT_SECRET')) {
|
||||
return ['ok'=>false, 'why'=>'spotify_constants_missing'];
|
||||
}
|
||||
if (SPOTIFY_CLIENT_ID === '' || SPOTIFY_CLIENT_SECRET === '') {
|
||||
return ['ok'=>false, 'why'=>'spotify_client_empty'];
|
||||
}
|
||||
|
||||
$ch = curl_init('https://accounts.spotify.com/api/token');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query([
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken,
|
||||
]),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Basic ' . base64_encode(SPOTIFY_CLIENT_ID . ':' . SPOTIFY_CLIENT_SECRET),
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
],
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
]);
|
||||
|
||||
$raw = curl_exec($ch);
|
||||
$curlErr = curl_error($ch);
|
||||
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$data = is_string($raw) ? json_decode($raw, true) : null;
|
||||
|
||||
if (!is_string($raw)) {
|
||||
return ['ok'=>false,'why'=>'curl_failed','curl_error'=>$curlErr ?: 'unknown'];
|
||||
}
|
||||
|
||||
if ($http < 200 || $http >= 300) {
|
||||
return ['ok'=>false,'why'=>'spotify_token_http','status'=>$http,'spotify'=>$data,'raw'=>$raw];
|
||||
}
|
||||
|
||||
if (!is_array($data) || empty($data['access_token'])) {
|
||||
return ['ok'=>false,'why'=>'spotify_token_bad_json','status'=>$http,'spotify'=>$data,'raw'=>$raw];
|
||||
}
|
||||
|
||||
return ['ok'=>true,'data'=>$data];
|
||||
}
|
||||
|
||||
function spotify_api_get(string $url, string $accessToken): array {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $accessToken,
|
||||
'Accept: application/json',
|
||||
],
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
]);
|
||||
|
||||
$raw = curl_exec($ch);
|
||||
$curlErr = curl_error($ch);
|
||||
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$data = is_string($raw) ? json_decode($raw, true) : null;
|
||||
return [$http, $data, $raw, $curlErr];
|
||||
}
|
||||
|
||||
try {
|
||||
// sanity: does pdo() exist?
|
||||
if (!function_exists('pdo')) safe_err('pdo_function_missing');
|
||||
|
||||
// what DB are we using?
|
||||
$dbName = pdo()->query("SELECT DATABASE()")->fetchColumn();
|
||||
if (!$dbName) safe_err('no_database_selected');
|
||||
|
||||
// does table exist?
|
||||
$stmt = pdo()->query("SHOW TABLES LIKE 'spotify_tokens'");
|
||||
$tbl = $stmt ? $stmt->fetchColumn() : null;
|
||||
if (!$tbl) safe_err('table_missing', ['db'=>$dbName]);
|
||||
|
||||
// fetch row
|
||||
$q = pdo()->query("SELECT refresh_token, access_token, access_expires FROM spotify_tokens WHERE id=1");
|
||||
if (!$q) safe_err('query_failed');
|
||||
$row = $q->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) safe_err('row_missing_id_1', ['db'=>$dbName]);
|
||||
|
||||
$refresh = trim((string)($row['refresh_token'] ?? ''));
|
||||
$access = trim((string)($row['access_token'] ?? ''));
|
||||
$exp = (int)($row['access_expires'] ?? 0);
|
||||
|
||||
if ($refresh === '') safe_err('refresh_token_empty_in_db', ['db'=>$dbName]);
|
||||
|
||||
// refresh if needed
|
||||
if ($access === '' || $exp <= (time() + 30)) {
|
||||
$new = spotify_refresh_access_token($refresh);
|
||||
if (empty($new['ok'])) {
|
||||
safe_err('refresh_failed', ['detail'=>$new, 'db'=>$dbName]);
|
||||
}
|
||||
|
||||
$access = (string)$new['data']['access_token'];
|
||||
$exp = time() + (int)($new['data']['expires_in'] ?? 3600);
|
||||
|
||||
$ok = pdo()->prepare("UPDATE spotify_tokens SET access_token=?, access_expires=? WHERE id=1")
|
||||
->execute([$access, $exp]);
|
||||
if (!$ok) safe_err('db_update_failed');
|
||||
}
|
||||
|
||||
// now playing
|
||||
[$http, $data, $raw, $curlErr] = spotify_api_get('https://api.spotify.com/v1/me/player/currently-playing', $access);
|
||||
|
||||
if ($http === 200 && is_array($data) && !empty($data['item'])) {
|
||||
$t = $data['item'];
|
||||
$artists = [];
|
||||
foreach (($t['artists'] ?? []) as $a) $artists[] = (string)($a['name'] ?? '');
|
||||
$img = (string)($t['album']['images'][0]['url'] ?? '');
|
||||
|
||||
json_out([
|
||||
'ok' => true,
|
||||
'mode' => !empty($data['is_playing']) ? 'playing' : 'recent',
|
||||
'track' => [
|
||||
'title' => (string)($t['name'] ?? ''),
|
||||
'artist' => trim(implode(', ', array_filter($artists))),
|
||||
'art' => $img,
|
||||
'url' => (string)($t['external_urls']['spotify'] ?? ''),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// fallback: recently played
|
||||
[$http2, $data2, $raw2, $curlErr2] = spotify_api_get('https://api.spotify.com/v1/me/player/recently-played?limit=1', $access);
|
||||
$item = (is_array($data2) && !empty($data2['items'][0]['track'])) ? $data2['items'][0]['track'] : null;
|
||||
|
||||
if ($http2 === 200 && is_array($item)) {
|
||||
$artists = [];
|
||||
foreach (($item['artists'] ?? []) as $a) $artists[] = (string)($a['name'] ?? '');
|
||||
$img = (string)($item['album']['images'][0]['url'] ?? '');
|
||||
|
||||
json_out([
|
||||
'ok' => true,
|
||||
'mode' => 'recent',
|
||||
'track' => [
|
||||
'title' => (string)($item['name'] ?? ''),
|
||||
'artist' => trim(implode(', ', array_filter($artists))),
|
||||
'art' => $img,
|
||||
'url' => (string)($item['external_urls']['spotify'] ?? ''),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
safe_err('no_track', [
|
||||
'currently_playing_http' => $http,
|
||||
'recent_http' => $http2
|
||||
]);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
safe_err('exception', [
|
||||
'type' => get_class($e),
|
||||
'msg' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user