435 lines
13 KiB
PHP
435 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\View;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* @group Endpoints
|
|
*
|
|
* API endpoints for serving admin-ajax blade content.
|
|
* These endpoints provide access to backend blade files that return JSON or HTML responses.
|
|
*/
|
|
class EndpointController extends Controller
|
|
{
|
|
/**
|
|
* List Available Endpoints
|
|
*
|
|
* Returns a list of all available admin-ajax endpoints that can be accessed via the API.
|
|
* Each endpoint corresponds to a blade file in the admin-ajax directory.
|
|
*
|
|
* The response includes all endpoints with their names, URLs, HTTP methods, response types, and categories.
|
|
*
|
|
* @responseFile storage/app/scribe/endpoints-response.json
|
|
*/
|
|
public function index()
|
|
{
|
|
$endpoints = $this->getAvailableEndpoints();
|
|
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'data' => [
|
|
'endpoints' => $endpoints,
|
|
'total' => count($endpoints)
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Execute Endpoint
|
|
*
|
|
* Executes the specified admin-ajax endpoint and returns the response.
|
|
* The response type is automatically detected:
|
|
* - If the output is valid JSON, returns JSON response
|
|
* - Otherwise, returns HTML response
|
|
*
|
|
* @urlParam endpoint string required The endpoint name (e.g., users, ndt-calculation). Example: users
|
|
*
|
|
* @queryParam _format string Optional. Force response format: 'json' or 'html'. Example: json
|
|
*
|
|
* @response 200 scenario="JSON Response" {
|
|
* "status": "success",
|
|
* "data": [{"id": 1, "name": "John Doe"}],
|
|
* "format": "json"
|
|
* }
|
|
*
|
|
* @response 200 scenario="HTML Response" {
|
|
* "status": "success",
|
|
* "html": "<div>Content here</div>",
|
|
* "format": "html"
|
|
* }
|
|
*
|
|
* @response 404 {
|
|
* "status": "error",
|
|
* "message": "Endpoint not found"
|
|
* }
|
|
*/
|
|
public function handle(Request $request, string $endpoint)
|
|
{
|
|
// Sanitize endpoint name
|
|
$endpoint = $this->sanitizeEndpoint($endpoint);
|
|
|
|
// Check if endpoint exists
|
|
if (!$this->endpointExists($endpoint)) {
|
|
return response()->json([
|
|
'status' => 'error',
|
|
'message' => 'Endpoint not found',
|
|
'endpoint' => $endpoint
|
|
], 404);
|
|
}
|
|
|
|
// SECURITY: Only allow readonly endpoints via API
|
|
if (!$this->isReadonlyEndpoint($endpoint)) {
|
|
return response()->json([
|
|
'status' => 'error',
|
|
'message' => 'This endpoint is not available via API. Only readonly endpoints are accessible.',
|
|
'endpoint' => $endpoint
|
|
], 403);
|
|
}
|
|
|
|
try {
|
|
// Render the blade view and capture output
|
|
$output = $this->renderEndpoint($endpoint, $request);
|
|
|
|
// Detect response format
|
|
$format = $request->input('_format');
|
|
|
|
if ($format === 'html') {
|
|
return $this->htmlResponse($output);
|
|
}
|
|
|
|
if ($format === 'json' || $this->isJson($output)) {
|
|
return $this->jsonResponse($output);
|
|
}
|
|
|
|
return $this->htmlResponse($output);
|
|
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'status' => 'error',
|
|
'message' => $e->getMessage(),
|
|
'endpoint' => $endpoint
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Endpoint Info
|
|
*
|
|
* Returns detailed information about a specific endpoint.
|
|
*
|
|
* @urlParam endpoint string required The endpoint name. Example: users
|
|
*
|
|
* @response 200 {
|
|
* "status": "success",
|
|
* "data": {
|
|
* "name": "users",
|
|
* "url": "/api/endpoints/users",
|
|
* "methods": ["GET", "POST"],
|
|
* "exists": true,
|
|
* "api_accessible": true,
|
|
* "path": "resources/views/admin-ajax/users.blade.php"
|
|
* }
|
|
* }
|
|
*/
|
|
public function info(Request $request, string $endpoint)
|
|
{
|
|
$endpoint = $this->sanitizeEndpoint($endpoint);
|
|
$exists = $this->endpointExists($endpoint);
|
|
$isReadonly = $exists ? $this->isReadonlyEndpoint($endpoint) : false;
|
|
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'data' => [
|
|
'name' => $endpoint,
|
|
'url' => "/api/endpoints/{$endpoint}",
|
|
'methods' => ['GET', 'POST'],
|
|
'exists' => $exists,
|
|
'api_accessible' => $isReadonly,
|
|
'path' => $exists ? "resources/views/admin-ajax/{$endpoint}.blade.php" : null,
|
|
'note' => !$isReadonly && $exists ? 'This endpoint is not marked as @api-readonly and cannot be accessed via API' : null
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Sanitize endpoint name to prevent directory traversal
|
|
*/
|
|
protected function sanitizeEndpoint(string $endpoint): string
|
|
{
|
|
// Remove any path traversal attempts
|
|
$endpoint = str_replace(['..', '/', '\\'], '', $endpoint);
|
|
|
|
// Convert to lowercase and replace spaces with dashes
|
|
$endpoint = Str::slug($endpoint);
|
|
|
|
return $endpoint;
|
|
}
|
|
|
|
/**
|
|
* Check if endpoint blade file exists
|
|
*/
|
|
protected function endpointExists(string $endpoint): bool
|
|
{
|
|
return View::exists("admin-ajax.{$endpoint}");
|
|
}
|
|
|
|
/**
|
|
* Check if endpoint is marked as readonly (safe for API access)
|
|
*
|
|
* Endpoints must have the annotation {{-- @api-readonly --}} at the beginning
|
|
* to be accessible via the API. This prevents write/delete operations from
|
|
* being exposed externally.
|
|
*/
|
|
protected function isReadonlyEndpoint(string $endpoint): bool
|
|
{
|
|
$path = resource_path("views/admin-ajax/{$endpoint}.blade.php");
|
|
|
|
if (!File::exists($path)) {
|
|
return false;
|
|
}
|
|
|
|
$content = File::get($path);
|
|
|
|
// Check for @api-readonly annotation
|
|
// Supports: {{-- @api-readonly --}} or <?php // @api-readonly
|
|
return Str::contains($content, '@api-readonly');
|
|
}
|
|
|
|
/**
|
|
* Get the file path for an endpoint
|
|
*/
|
|
protected function getEndpointPath(string $endpoint): ?string
|
|
{
|
|
// Check main directory
|
|
$path = resource_path("views/admin-ajax/{$endpoint}.blade.php");
|
|
if (File::exists($path)) {
|
|
return $path;
|
|
}
|
|
|
|
// Check subdirectories
|
|
$parts = explode('/', $endpoint);
|
|
if (count($parts) > 1) {
|
|
$subPath = resource_path("views/admin-ajax/" . implode('/', $parts) . ".blade.php");
|
|
if (File::exists($subPath)) {
|
|
return $subPath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Render the endpoint blade file
|
|
*/
|
|
protected function renderEndpoint(string $endpoint, Request $request): string
|
|
{
|
|
// Start output buffering to capture any echo/print statements
|
|
ob_start();
|
|
|
|
try {
|
|
// Render the view with request data
|
|
$view = View::make("admin-ajax.{$endpoint}", [
|
|
'request' => $request
|
|
]);
|
|
|
|
// Get rendered content
|
|
$rendered = $view->render();
|
|
|
|
// Get any buffered output (from echo statements in blade)
|
|
$buffered = ob_get_clean();
|
|
|
|
// Return buffered output if exists, otherwise rendered content
|
|
return !empty($buffered) ? $buffered : $rendered;
|
|
|
|
} catch (\Exception $e) {
|
|
ob_end_clean();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if string is valid JSON
|
|
*/
|
|
protected function isJson(string $string): bool
|
|
{
|
|
if (empty($string)) {
|
|
return false;
|
|
}
|
|
|
|
$string = trim($string);
|
|
|
|
// Quick check for JSON structure
|
|
if (!in_array($string[0], ['{', '['])) {
|
|
return false;
|
|
}
|
|
|
|
json_decode($string);
|
|
return json_last_error() === JSON_ERROR_NONE;
|
|
}
|
|
|
|
/**
|
|
* Return JSON response
|
|
*/
|
|
protected function jsonResponse(string $output)
|
|
{
|
|
$data = json_decode($output, true);
|
|
|
|
// If already has status wrapper, return as-is
|
|
if (is_array($data) && isset($data['status'])) {
|
|
return response()->json($data);
|
|
}
|
|
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'data' => $data,
|
|
'format' => 'json'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Return HTML response
|
|
*/
|
|
protected function htmlResponse(string $output)
|
|
{
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'html' => $output,
|
|
'format' => 'html'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get all available endpoints from admin-ajax directory
|
|
* Only returns endpoints marked with @api-readonly annotation
|
|
*/
|
|
public function getAvailableEndpoints(): array
|
|
{
|
|
$endpoints = [];
|
|
$path = resource_path('views/admin-ajax');
|
|
|
|
if (!File::isDirectory($path)) {
|
|
return $endpoints;
|
|
}
|
|
|
|
// Scan all blade files recursively
|
|
$this->scanEndpointsRecursive($path, '', $endpoints);
|
|
|
|
// Sort alphabetically
|
|
usort($endpoints, fn($a, $b) => strcmp($a['name'], $b['name']));
|
|
|
|
return $endpoints;
|
|
}
|
|
|
|
/**
|
|
* Recursively scan directory for readonly endpoints
|
|
*/
|
|
protected function scanEndpointsRecursive(string $basePath, string $prefix, array &$endpoints): void
|
|
{
|
|
$files = File::files($basePath);
|
|
|
|
foreach ($files as $file) {
|
|
$filename = $file->getFilename();
|
|
|
|
// Only process .blade.php files
|
|
if (!Str::endsWith($filename, '.blade.php')) {
|
|
continue;
|
|
}
|
|
|
|
// Extract endpoint name
|
|
$name = str_replace('.blade.php', '', $filename);
|
|
$fullName = $prefix ? "{$prefix}/{$name}" : $name;
|
|
|
|
// Check file content for @api-readonly annotation
|
|
$content = File::get($file->getPathname());
|
|
|
|
// SECURITY: Only include endpoints marked as readonly
|
|
if (!Str::contains($content, '@api-readonly')) {
|
|
continue;
|
|
}
|
|
|
|
$endpoints[] = [
|
|
'name' => $fullName,
|
|
'url' => "/api/endpoints/{$fullName}",
|
|
'methods' => ['GET', 'POST']
|
|
];
|
|
}
|
|
|
|
// Scan subdirectories
|
|
$directories = File::directories($basePath);
|
|
foreach ($directories as $dir) {
|
|
$dirName = basename($dir);
|
|
$newPrefix = $prefix ? "{$prefix}/{$dirName}" : $dirName;
|
|
$this->scanEndpointsRecursive($dir, $newPrefix, $endpoints);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get endpoints grouped by category (for documentation)
|
|
*/
|
|
public function getEndpointsByCategory(): array
|
|
{
|
|
$endpoints = $this->getAvailableEndpoints();
|
|
$categories = [];
|
|
|
|
foreach ($endpoints as $endpoint) {
|
|
$name = $endpoint['name'];
|
|
|
|
// Determine category based on prefix
|
|
$category = $this->categorizeEndpoint($name);
|
|
|
|
if (!isset($categories[$category])) {
|
|
$categories[$category] = [];
|
|
}
|
|
|
|
$categories[$category][] = $endpoint;
|
|
}
|
|
|
|
ksort($categories);
|
|
return $categories;
|
|
}
|
|
|
|
/**
|
|
* Categorize endpoint based on name prefix
|
|
*/
|
|
protected function categorizeEndpoint(string $name): string
|
|
{
|
|
$prefixes = [
|
|
'ndt-' => 'NDT Operations',
|
|
'welder-' => 'Welder Management',
|
|
'weld' => 'Welding',
|
|
'register-' => 'Register Creator',
|
|
'test-package' => 'Test Packages',
|
|
'tp-' => 'Test Packages',
|
|
'document-' => 'Document Management',
|
|
'report-' => 'Reports',
|
|
'summary' => 'Summaries',
|
|
'excel-' => 'Excel Operations',
|
|
'pdf' => 'PDF Operations',
|
|
'user' => 'User Management',
|
|
'permission' => 'Permissions',
|
|
'content-' => 'Content Management',
|
|
'spool-' => 'Spool Operations',
|
|
'material-' => 'Materials',
|
|
'paint-' => 'Paint Operations',
|
|
'repair-' => 'Repair Operations',
|
|
'rfi-' => 'RFI Operations',
|
|
'punch-' => 'Punch Lists',
|
|
'mto' => 'MTO Operations',
|
|
];
|
|
|
|
foreach ($prefixes as $prefix => $category) {
|
|
if (Str::startsWith($name, $prefix)) {
|
|
return $category;
|
|
}
|
|
}
|
|
|
|
return 'General';
|
|
}
|
|
}
|
|
|