diff --git a/app/Http/Controllers/api/FolderController.php b/app/Http/Controllers/api/FolderController.php index 2a30af7..2464fe6 100644 --- a/app/Http/Controllers/api/FolderController.php +++ b/app/Http/Controllers/api/FolderController.php @@ -5,8 +5,12 @@ namespace App\Http\Controllers\api; use App\Http\Controllers\Controller; use App\Models\Folder; use App\Models\FolderArt; +use App\Models\FolderBylaw; +use App\Models\FolderJudicialPrecedent; +use App\Models\FolderLegalOpinion; use App\Traits\BaseApiResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; class FolderController extends Controller { @@ -14,12 +18,18 @@ class FolderController extends Controller public function index() { - $folder = Folder::query()->where('user_id', auth()->id())->get() + // Eager load everything to prevent N+1 Database issues + $folder = Folder::with(['arts', 'bylaws', 'judicialPrecedents', 'legalOpinions']) + ->where('user_id', auth()->id()) + ->get() ->map(function ($q) { return [ 'id' => $q->id, 'name' => $q->name, 'count_of_arts' => $q->arts->count(), + 'count_of_bylaws' => $q->bylaws->count(), + 'count_of_judicials' => $q->judicialPrecedents->count(), + 'count_of_opinions' => $q->legalOpinions->count(), 'created_at' => $q->created_at, ]; }); @@ -34,7 +44,7 @@ class FolderController extends Controller ]); Folder::create([ - 'user_id' => auth()->user()->id, + 'user_id' => auth()->id(), 'name' => $validted['name'], ]); @@ -44,40 +54,72 @@ class FolderController extends Controller public function assign(Request $request) { $validted = $request->validate([ - 'folder_id' => 'required', - 'art_id' => 'required', + 'folder_id' => 'required', + 'art_id' => 'nullable', + 'bylaw_document_id' => 'nullable', + 'judicial_precedent_id' => 'nullable', + 'legal_opinion_id' => 'nullable', ]); - if (Folder::query()->where('id', $validted['folder_id'])->count() == 0) { - return $this->failed([], 'پوشه مورد نظر یافت نشد', 404); + // 🚨 SECURITY FIX: Check if the folder exists AND belongs to the logged-in user + if (Folder::where('id', $validted['folder_id'])->where('user_id', auth()->id())->count() == 0) { + return $this->failed([], 'پوشه مورد نظر یافت نشد یا شما دسترسی ندارید', 404); } - if (FolderArt::query()->where('folder_id', $validted['folder_id'])->where('art_id', $validted['art_id'])->count() > 0) { - return $this->failed([], 'ارت مورد نظر قبلا اضافه شده است', 400); + if ($request->filled('art_id')) { + if (FolderArt::where('folder_id', $validted['folder_id'])->where('art_id', $validted['art_id'])->exists()) { + return $this->failed([], 'ارت مورد نظر قبلا اضافه شده است', 400); + } + FolderArt::create(['folder_id' => $validted['folder_id'], 'art_id' => $validted['art_id']]); + } + elseif ($request->filled('bylaw_document_id')) { + if (FolderBylaw::where('folder_id', $validted['folder_id'])->where('bylaw_document_id', $validted['bylaw_document_id'])->exists()) { + return $this->failed([], 'آیین‌نامه قبلا اضافه شده است', 400); + } + FolderBylaw::create(['folder_id' => $validted['folder_id'], 'bylaw_document_id' => $validted['bylaw_document_id']]); + } + elseif ($request->filled('judicial_precedent_id')) { + if (FolderJudicialPrecedent::where('folder_id', $validted['folder_id'])->where('judicial_precedent_id', $validted['judicial_precedent_id'])->exists()) { + return $this->failed([], 'رأی قبلا اضافه شده است', 400); + } + FolderJudicialPrecedent::create(['folder_id' => $validted['folder_id'], 'judicial_precedent_id' => $validted['judicial_precedent_id']]); + } + elseif ($request->filled('legal_opinion_id')) { + if (FolderLegalOpinion::where('folder_id', $validted['folder_id'])->where('legal_opinion_id', $validted['legal_opinion_id'])->exists()) { + return $this->failed([], 'نظریه قبلا اضافه شده است', 400); + } + FolderLegalOpinion::create(['folder_id' => $validted['folder_id'], 'legal_opinion_id' => $validted['legal_opinion_id']]); + } + else { + return $this->failed([], 'موردی برای افزودن یافت نشد', 400); } - FolderArt::create([ - 'folder_id' => $validted['folder_id'], - 'art_id' => $validted['art_id'], - ]); - - return $this->success([], 'اضافه شد', 'ارت با موفیت اضافه شد'); + return $this->success([], 'اضافه شد', 'با موفیت اضافه شد'); } public function folder($id) { - $arts = FolderArt::where('folder_id', $id) - ->with('arts.chapter', 'arts.part', 'arts.volum', 'arts.law', 'arts.book', 'arts.section', 'arts.gate') - ->get() - ->pluck('arts') - ->flatten(); + // 🚨 SECURITY FIX: Check user_id when fetching the folder contents + $folder = Folder::with([ + 'arts.chapter', 'arts.part', 'arts.volum', 'arts.law', 'arts.book', 'arts.section', 'arts.gate', + 'bylaws.category', 'bylaws.approvalAuthority', 'bylaws.promulgationAuthority', + 'judicialPrecedents.category', + 'legalOpinions.category' + ])->where('user_id', auth()->id())->find($id); - $arts = $arts->map(function ($art) { + if (!$folder) { + return $this->failed([], 'پوشه یافت نشد یا شما دسترسی ندارید', 404); + } + + $items = collect(); + + // 1. Process Arts + $arts = $folder->arts->map(function ($art) { $text = $art->text ?? ''; $shortText = ''; if (strlen($text) > 50) { - $shortText = substr($text, 0, 50); - $shortText = substr($shortText, 0, strrpos($shortText, ' ')).'...'; + $shortText = mb_substr($text, 0, 50); // Changed to mb_substr for Persian text safety + $shortText = mb_substr($shortText, 0, mb_strrpos($shortText, ' ')).'...'; } else { $shortText = $text; } @@ -89,6 +131,8 @@ class FolderController extends Controller 'text' => $isLocked ? null : $shortText, 'number' => $art->number, 'is_locked' => $isLocked, + 'type' => 'art', + 'assigned_at' => $art->pivot->created_at, 'chapter' => $art->chapter != null ? [ 'id' => $art->chapter->id, 'title' => $art->chapter->title, @@ -126,27 +170,193 @@ class FolderController extends Controller ] : null, ]; }); + $items = $items->concat($arts); - return $this->success($arts); + // 2. Process Bylaws + $bylawIds = $folder->bylaws->pluck('id')->toArray(); + $allBylawNodes = collect(); + if (!empty($bylawIds)) { + $allBylawNodes = DB::table('bylaw_nodes') + ->whereIn('document_id', $bylawIds) + ->orderBy('sort_order', 'asc') + ->get(); + } + + $bylaws = $folder->bylaws->map(function ($bylaw) use ($allBylawNodes) { + $itemNodes = $allBylawNodes->where('document_id', $bylaw->id); + $formatted = $this->formatBylaw($bylaw, $itemNodes); + $formatted['type'] = 'bylaw'; + $formatted['assigned_at'] = $bylaw->pivot->created_at; + return $formatted; + }); + $items = $items->concat($bylaws); + + // 3. Process Judicial Precedents + $judicials = $folder->judicialPrecedents->map(function ($judicial) { + $formatted = $this->formatJudicial($judicial); + $formatted['type'] = 'judicial_precedent'; + $formatted['assigned_at'] = $judicial->pivot->created_at; + return $formatted; + }); + $items = $items->concat($judicials); + + // 4. Process Legal Opinions + $opinions = $folder->legalOpinions->map(function ($opinion) { + $formatted = $this->formatOpinion($opinion); + $formatted['type'] = 'legal_opinion'; + $formatted['assigned_at'] = $opinion->pivot->created_at; + return $formatted; + }); + $items = $items->concat($opinions); + + // Sort everything together based on when it was added to the folder! + $sortedItems = $items->sortByDesc('assigned_at')->values()->toArray(); + + return $this->success($sortedItems); } public function delete_folder($id) { - if (Folder::query()->where('id', $id)->count() == 0) { - return $this->failed('پوشه یافت نشد', 'پوشه مورد نظر یافت نشد', 404); - } - Folder::query()->where('id', $id)->delete(); + // 🚨 SECURITY FIX + $folder = Folder::where('id', $id)->where('user_id', auth()->id())->first(); + if (!$folder) { + return $this->failed('پوشه یافت نشد', 'پوشه مورد نظر یافت نشد یا شما دسترسی ندارید', 404); + } + + $folder->delete(); return $this->success([], 'حذف شد', 'پوشه با موفقیت حذف شد'); } + // 🚨 SECURITY FIX FOR ALL PIVOT DELETES: Verify the folder belongs to the user first! + private function checkFolderOwnership($folder_id) + { + return Folder::where('id', $folder_id)->where('user_id', auth()->id())->exists(); + } + public function delete_art($id, $art_id) { - if (FolderArt::query()->where('folder_id', $id)->where('art_id', $art_id)->count() == 0) { - return $this->failed([], 'ارت مورد نظر یافت نشد', 404); - } - FolderArt::query()->where('folder_id', $id)->where('art_id', $art_id)->delete(); + if (!$this->checkFolderOwnership($id)) return $this->failed([], 'عدم دسترسی', 403); - return $this->success([], 'حذف شد', 'ارت با موفقیت حذف شد'); + if (FolderArt::where('folder_id', $id)->where('art_id', $art_id)->count() == 0) { + return $this->failed([], 'مورد نظر یافت نشد', 404); + } + FolderArt::where('folder_id', $id)->where('art_id', $art_id)->delete(); + return $this->success([], 'حذف شد', 'با موفقیت حذف شد'); + } + + public function delete_bylaw($id, $bylaw_id) + { + if (!$this->checkFolderOwnership($id)) return $this->failed([], 'عدم دسترسی', 403); + + if (FolderBylaw::where('folder_id', $id)->where('bylaw_document_id', $bylaw_id)->count() == 0) { + return $this->failed([], 'مورد نظر یافت نشد', 404); + } + FolderBylaw::where('folder_id', $id)->where('bylaw_document_id', $bylaw_id)->delete(); + return $this->success([], 'حذف شد', 'با موفقیت حذف شد'); + } + + public function delete_judicial($id, $judicial_id) + { + if (!$this->checkFolderOwnership($id)) return $this->failed([], 'عدم دسترسی', 403); + + if (FolderJudicialPrecedent::where('folder_id', $id)->where('judicial_precedent_id', $judicial_id)->count() == 0) { + return $this->failed([], 'مورد نظر یافت نشد', 404); + } + FolderJudicialPrecedent::where('folder_id', $id)->where('judicial_precedent_id', $judicial_id)->delete(); + return $this->success([], 'حذف شد', 'با موفقیت حذف شد'); + } + + public function delete_opinion($id, $opinion_id) + { + if (!$this->checkFolderOwnership($id)) return $this->failed([], 'عدم دسترسی', 403); + + if (FolderLegalOpinion::where('folder_id', $id)->where('legal_opinion_id', $opinion_id)->count() == 0) { + return $this->failed([], 'مورد نظر یافت نشد', 404); + } + FolderLegalOpinion::where('folder_id', $id)->where('legal_opinion_id', $opinion_id)->delete(); + return $this->success([], 'حذف شد', 'با موفقیت حذف شد'); + } + + /* ----------------------------------------------------------------- + * FORMATTING HELPERS (Safely cloned from GlobalSearch) + * ----------------------------------------------------------------- */ + + private function isLockedForCurrentUserGeneric($item): bool + { + if (auth()->check() && auth()->user() && method_exists(auth()->user(), 'isSubscriber')) { + return !auth()->user()->isSubscriber() && (bool) ($item->is_locked ?? false); + } + return (bool) ($item->is_locked ?? false); + } + + private function formatBylaw($item, $itemNodes = null) + { + $isLocked = $this->isLockedForCurrentUserGeneric($item); + $description = $item->description; + + $cleanDesc = trim(str_replace([' ', '‌', "\r", "\n", "\t", ' '], '', strip_tags($description ?? ''))); + + if (strlen($cleanDesc) < 5) { + if ($itemNodes === null || (is_object($itemNodes) && $itemNodes->isEmpty())) { + $dbNodes = DB::table('bylaw_nodes')->where('document_id', $item->id)->count(); + if ($dbNodes > 0) { + $itemNodes = DB::table('bylaw_nodes')->where('document_id', $item->id)->orderBy('sort_order', 'asc')->get(); + } + } + $nodeContents = []; + if ($itemNodes) { + foreach ($itemNodes as $node) { + $content = trim(strip_tags($node->node_content ?? '')); + if (!empty($content)) { + $nodeContents[] = $content; + } + } + } + $description = implode("\n\n", $nodeContents); + } + + return [ + 'id' => $item->id, + 'title' => $item->title, + 'category_name' => optional($item->category)->name, + 'approval_authority_name' => optional($item->approvalAuthority)->name, + 'approval_date' => $item->approval_date, + 'promulgation_number' => $item->promulgation_number, + 'description' => $isLocked ? null : mb_substr($description, 0, 100) . '...', + 'is_locked' => $isLocked, + ]; + } + + private function formatJudicial($item) + { + $isLocked = $this->isLockedForCurrentUserGeneric($item); + + return [ + 'id' => $item->id, + 'title' => $item->subject, + 'ruling_number' => $item->ruling_number, + 'ruling_date' => $item->ruling_date, + 'text' => $isLocked ? null : mb_substr($item->full_text, 0, 100) . '...', + 'issuing_authority' => $item->issuing_authority, + 'category_name' => optional($item->category)->name, + 'is_locked' => $isLocked, + ]; + } + + private function formatOpinion($item) + { + $isLocked = $this->isLockedForCurrentUserGeneric($item); + + return [ + 'id' => $item->id, + 'title' => $item->subject, + 'opinion_number' => $item->opinion_number, + 'opinion_date' => $item->opinion_date, + 'text' => $isLocked ? null : mb_substr($item->full_text, 0, 100) . '...', + 'issuing_authority' => $item->issuing_authority, + 'category_name' => optional($item->category)->name, + 'is_locked' => $isLocked, + ]; } } diff --git a/app/Models/Folder.php b/app/Models/Folder.php index c5437b5..b75de0a 100644 --- a/app/Models/Folder.php +++ b/app/Models/Folder.php @@ -16,6 +16,21 @@ class Folder extends Model public function arts() { - return $this->belongsToMany(Art::class, 'folder_art'); + return $this->belongsToMany(Art::class, 'folder_art')->withTimestamps(); + } + + public function bylaws() + { + return $this->belongsToMany(BylawDocument::class, 'folder_bylaw', 'folder_id', 'bylaw_document_id')->withTimestamps(); + } + + public function judicialPrecedents() + { + return $this->belongsToMany(JudicialPrecedent::class, 'folder_judicial_precedent', 'folder_id', 'judicial_precedent_id')->withTimestamps(); + } + + public function legalOpinions() + { + return $this->belongsToMany(LegalOpinion::class, 'folder_legal_opinion', 'folder_id', 'legal_opinion_id')->withTimestamps(); } } diff --git a/app/Models/FolderBylaw.php b/app/Models/FolderBylaw.php new file mode 100644 index 0000000..7144529 --- /dev/null +++ b/app/Models/FolderBylaw.php @@ -0,0 +1,13 @@ +middleware('auth:sanctum')->group(function () { Route::delete('/opinion-notes/{id}', [\App\Http\Controllers\api\LegalOpinionNoteController::class, 'destroy']); + Route::delete('/folders/{id}/bylaw/{bylaw_id}', [\App\Http\Controllers\api\FolderController::class, 'delete_bylaw']); + Route::delete('/folders/{id}/judicial/{judicial_id}', [\App\Http\Controllers\api\FolderController::class, 'delete_judicial']); + Route::delete('/folders/{id}/opinion/{opinion_id}', [\App\Http\Controllers\api\FolderController::class, 'delete_opinion']); });