JSON { ok, recordings:[ {callId,caller,callee,extension,startedAt,durationSeconds,audioUrl,downloadUrl} ] } * 2) Serve: ?file=&dl=0|1 -> streams the .wav/.mp3 for playback/download * * Security: every request must carry the shared token, either as * Authorization: Bearer (the app sends this for the list) * or ?token= (used inside audioUrl for the browser) */ // ==== EDIT THIS ONE LINE ==== $TOKEN = 'MegaTrustRec_2026_ChangeMe'; // must match PBX_RECORDINGS_API_TOKEN in the app // ============================ $MONITOR = '/var/spool/asterisk/monitor'; // FreePBX default recordings folder header('Access-Control-Allow-Origin: *'); /* ---------- auth ---------- */ function bearer_token() { $auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; if (!$auth && function_exists('apache_request_headers')) { $h = apache_request_headers(); foreach ($h as $k => $v) { if (strtolower($k) === 'authorization') { $auth = $v; break; } } } return preg_replace('/^Bearer\s+/i', '', trim($auth)); } $provided = $_GET['token'] ?? bearer_token(); if ($TOKEN !== '' && !hash_equals($TOKEN, (string) $provided)) { http_response_code(401); header('Content-Type: application/json'); echo json_encode(['ok' => false, 'error' => 'unauthorized']); exit; } /* ---------- read FreePBX DB credentials ---------- */ function freepbx_db() { $c = ['host' => '127.0.0.1', 'user' => 'freepbxuser', 'pass' => '', 'cdr' => 'asteriskcdrdb']; foreach (['/etc/freepbx.conf', '/etc/amportal.conf'] as $f) { if (!is_readable($f)) continue; $s = file_get_contents($f); // /etc/freepbx.conf -> $amp_conf['AMPDBPASS'] = 'xxx'; // /etc/amportal.conf -> AMPDBPASS=xxx if (preg_match("/AMPDBUSER['\"\]\s=]+['\"]?([A-Za-z0-9_.\-]+)/i", $s, $m)) $c['user'] = $m[1]; if (preg_match("/AMPDBPASS['\"\]\s=]+['\"]?([^'\"\s;#]+)/i", $s, $m)) $c['pass'] = $m[1]; if (preg_match("/AMPDBHOST['\"\]\s=]+['\"]?([A-Za-z0-9_.\-]+)/i", $s, $m)) $c['host'] = $m[1]; } // The CDR database in FreePBX is always 'asteriskcdrdb' (same MySQL user). $c['cdr'] = 'asteriskcdrdb'; return $c; } /* ---------- resolve a recording file safely under $MONITOR ---------- */ function find_recording($MONITOR, $recordingfile, $calldate) { $base = basename((string) $recordingfile); // never trust the path if ($base === '') return null; // FreePBX stores under monitor/YYYY/MM/DD/ $sub = $calldate ? date('Y/m/d', strtotime($calldate)) : ''; $candidates = []; if ($sub) $candidates[] = "$MONITOR/$sub/$base"; $candidates[] = "$MONITOR/$base"; foreach ($candidates as $p) { $real = realpath($p); if ($real && strpos($real, realpath($MONITOR)) === 0 && is_file($real)) return $real; } // last resort: shallow recursive search by basename $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($MONITOR, FilesystemIterator::SKIP_DOTS)); foreach ($it as $file) { if ($file->getFilename() === $base) return $file->getPathname(); } return null; } /* ==================== MODE 2: serve an audio file ==================== */ if (isset($_GET['file'])) { $rel = $_GET['file']; $real = find_recording($MONITOR, $rel, $_GET['d'] ?? null); if (!$real) { http_response_code(404); echo 'not found'; exit; } $ext = strtolower(pathinfo($real, PATHINFO_EXTENSION)); $mime = $ext === 'mp3' ? 'audio/mpeg' : ($ext === 'gsm' ? 'audio/gsm' : 'audio/wav'); $size = filesize($real); header('Content-Type: ' . $mime); header('Accept-Ranges: bytes'); if (!empty($_GET['dl'])) header('Content-Disposition: attachment; filename="' . basename($real) . '"'); // basic HTTP Range support so playback seeking works if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $m)) { $start = (int) $m[1]; $end = $m[2] === '' ? $size - 1 : (int) $m[2]; $len = $end - $start + 1; http_response_code(206); header("Content-Range: bytes $start-$end/$size"); header('Content-Length: ' . $len); $fp = fopen($real, 'rb'); fseek($fp, $start); echo fread($fp, $len); fclose($fp); } else { header('Content-Length: ' . $size); readfile($real); } exit; } /* ==================== MODE 1: list recordings ==================== */ header('Content-Type: application/json'); $extension = preg_replace('/\D/', '', $_GET['extension'] ?? ''); $date = preg_replace('/[^0-9\-]/', '', $_GET['date'] ?? date('Y-m-d')); if ($extension === '') { echo json_encode(['ok' => true, 'recordings' => []]); exit; } $db = freepbx_db(); try { $pdo = new PDO( "mysql:host={$db['host']};dbname={$db['cdr']};charset=utf8", $db['user'], $db['pass'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); } catch (Throwable $e) { http_response_code(500); echo json_encode(['ok' => false, 'error' => 'DB connect failed: ' . $e->getMessage()]); exit; } $like = '%/' . $extension . '-%'; $stmt = $pdo->prepare( "SELECT uniqueid, calldate, src, dst, duration, billsec, disposition, recordingfile FROM cdr WHERE recordingfile <> '' AND DATE(calldate) = :d AND (src = :e OR dst = :e OR channel LIKE :l OR dstchannel LIKE :l) ORDER BY calldate DESC LIMIT 500" ); $stmt->execute([':d' => $date, ':e' => $extension, ':l' => $like]); $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'https'; $self = $scheme . '://' . ($_SERVER['HTTP_HOST'] ?? 'pbx.megatrust.live') . ($_SERVER['SCRIPT_NAME'] ?? '/recordings.php'); $out = []; foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) { $base = basename((string) $r['recordingfile']); if ($base === '') continue; $q = http_build_query(['token' => $TOKEN, 'file' => $base, 'd' => $r['calldate']]); $out[] = [ 'callId' => $r['uniqueid'], 'caller' => $r['src'], 'callee' => $r['dst'], 'extension' => $extension, 'startedAt' => $r['calldate'], // durationSeconds = ACTUAL talk time (billsec). A no-answer call has // billsec = 0 and an empty recording, so the app filters those out. 'durationSeconds' => (int) $r['billsec'], 'billsecSeconds' => (int) $r['billsec'], 'callSeconds' => (int) $r['duration'], // total incl. ring, for reference 'disposition' => $r['disposition'], 'audioUrl' => "$self?$q", 'downloadUrl' => "$self?$q&dl=1", ]; } echo json_encode(['ok' => true, 'count' => count($out), 'recordings' => $out]);