İlk temizlik tamamlandı bir önceki projeden
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user