From 3178904b28058e60e94c50e6de5193964d6067cb Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 21 Jun 2026 10:53:23 +0000 Subject: [PATCH] feat: place generated report in the CaseFiles case folder (P7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DirectAccessReportGenerator now, when CaseFilesCore is installed (soft-detected, no hard dependency), sets the generated Document's folderId to the case "מסמכים" folder via CaseFolderService so it shows in the CaseDocs panel. The existing Case link + best-effort NetworkStorage upload are unchanged. Verified on dev (P7_OK): php -l clean; Case.documents relate is visible via Document.cases (shared junction); folder placement works. Co-Authored-By: Claude Opus 4.8 (1M context) --- .taskmaster/tasks/tasks.json | 52 ++++++++++++++++--- ARCHITECTURE.md | 34 ++++++++++++ README.md | 39 ++++++++++++++ .../Tools/DirectAccessReportGenerator.php | 14 +++++ 4 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 README.md diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 689d968..e27a980 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -2,26 +2,66 @@ "master": { "tasks": [ { - "id": "1", + "id": 1, "title": "fix: subject-only direct-access report naming (tight hyphen) + case-folder upload", "description": "Report file uses subject-only canonical naming and is uploaded to the case folder correctly.", "details": "DirectAccessReportGenerator: em-dash -> tight hyphen; sanitize via LocalFilesystemClient::sanitizeFileName; save Document with skipNetworkUpload, relate to Case, explicit uploadDocument + DB realign to canonical stored name.", "testStrategy": "", - "status": "in-progress", + "status": "done", "dependencies": [], "priority": "high", "subtasks": [], - "updatedAt": "2026-06-03T07:30:42.898Z" + "updatedAt": "2026-06-03T08:19:08.135Z" + }, + { + "id": 2, + "title": "תיקון אבטחה: הסרת מחיקת טבלאות ועמודות מ-AfterUninstall", + "description": "הסרת הקריאות ל-dropTables ו-cleanCaseColumns מ-AfterUninstall.php כדי למנוע אובדן נתונים בלתי הפיך בעת הסרה/התקנה מחדש של ההרחבה.", + "details": "## רקע\n\nהממצא F-002 בביקורת האבטחה (SECURITY_AUDIT_2026-04-25.md:137-168) מזהה שהסקריפט `scripts/AfterUninstall.php` מבצע פעולות הרסניות:\n\n1. **מחיקת טבלאות** (שורות 54-73): `DROP TABLE legal_aid` ו-`DROP TABLE direct_access_report`\n2. **מחיקת עמודות** (שורות 75-101): `ALTER TABLE case DROP COLUMN` על 5 עמודות (`c_legal_aid_type`, `c_aid_type`, `c_urgency_level`, `c_appointment_date`, `c_filing_deadline`)\n\nזה מפר את **כלל N2** מ-EXTENSION_DEVELOPMENT_RULES.md:\n> כאשר הרחבה מוסיפה שדות לישויות ליבה (Case, Account, Contact וכו'), הסקריפט AfterUninstall חייב להסיר רק את ההפניות מ-layouts אבל **להשאיר את עמודות הדאטאבייס**, כך שנתוני הלקוח ישרדו מחזור הסרה/התקנה מחדש.\n\n## שינויים נדרשים\n\n### 1. הסרת הפונקציה `dropTables()` (שורות 54-73)\nמחק את כל הפונקציה והקריאה אליה בשורה 31.\n\n### 2. הסרת הפונקציה `cleanCaseColumns()` (שורות 75-101)\nמחק את כל הפונקציה והקריאה אליה בשורה 34.\n\n### 3. הוספת פונקציית ניקוי layouts (אופציונלי)\nאם יש layouts ב-`custom/Espo/Custom/Resources/layouts/Case/*.json` שמפנים לשדות שנוספו, יש להסיר את ההפניות:\n\n```php\nprivate function removeLayoutReferences(Log $log): void\n{\n $layoutDir = 'custom/Espo/Custom/Resources/layouts/Case';\n if (!is_dir($layoutDir)) {\n return;\n }\n\n $fieldsToRemove = [\n 'cLegalAidType',\n 'cAidType',\n 'cUrgencyLevel',\n 'cAppointmentDate',\n 'cFilingDeadline',\n ];\n\n foreach (glob($layoutDir . '/*.json') as $file) {\n $content = file_get_contents($file);\n $layout = json_decode($content, true);\n if (!$layout) continue;\n\n $modified = $this->removeFieldsFromLayout($layout, $fieldsToRemove);\n if ($modified) {\n file_put_contents($file, json_encode($layout, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));\n $log->info(\"LegalAssistance: Removed field references from {$file}\");\n }\n }\n}\n\nprivate function removeFieldsFromLayout(array &$layout, array $fields): bool\n{\n $modified = false;\n foreach ($layout as $key => &$value) {\n if (is_array($value)) {\n if (isset($value['name']) && in_array($value['name'], $fields)) {\n unset($layout[$key]);\n $modified = true;\n } else {\n $modified = $this->removeFieldsFromLayout($value, $fields) || $modified;\n }\n }\n }\n $layout = array_values($layout); // Re-index\n return $modified;\n}\n```\n\n### 4. עדכון תיעוד הקוד בראש הקובץ\nשנה את התיאור ב-PHPDoc להסביר שהסקריפט מסיר רק:\n- רשומות DocumentTemplate\n- קבצי i18n\n- הפניות ב-layouts (אם רלוונטי)\n- וניקוי cache\n\n**לא** מוחק טבלאות או עמודות.\n\n## מבנה הקובץ לאחר התיקון\n\n```php\ncheckScope('Case', 'read')`).\n\n**וקטור התקיפה:** כל משתמש EspoCRM מאומת עם הרשאת קריאה לתיקים יכול:\n1. לקרוא ל-`POST /api/v1/SmartAssistant/action/executeTool` עם `caseId` של תיק שאינו שלו\n2. הכלי קורא ל-`getEntityById('Case', $caseId)` שעוקף ACL כברירת מחדל\n3. מסמך DOCX מזויף נוצר ומצורף לתיק הקורבן\n4. המסמך מכיל תוכן שנשלט על ידי התוקף אך נושא את שם עורך הדין המוקצה לתיק\n\n## שינויים נדרשים\n\n### 1. הזרקת AclManager לקונסטרקטור\n\n```php\n// files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/DirectAccessReportGenerator.php\n\nuse Espo\\Core\\Acl;\nuse Espo\\Core\\Exceptions\\Forbidden;\n\n// ...\n\npublic function __construct(\n private EntityManager $entityManager,\n private InjectableFactory $injectableFactory,\n private Log $log,\n private User $user,\n private Acl $acl // הוסף\n) {}\n```\n\n### 2. בדיקת הרשאות ברמת רשומה בתחילת execute()\n\nבתחילת המתודה `execute()`, מיד אחרי שליפת התיק (שורה 41-44):\n\n```php\npublic function execute(array $params, ?string $caseId, string $userId): array\n{\n if (!$caseId) {\n throw new Error('generate_direct_access_report requires a case context.');\n }\n\n $this->log->info(\"LegalAssistance: Generating direct access report for case {$caseId}\");\n\n $case = $this->entityManager->getEntityById('Case', $caseId);\n if (!$case) {\n throw new Error(\"Case {$caseId} not found.\");\n }\n\n // === תיקון אבטחה F-001: בדיקת ACL ברמת רשומה ===\n // נדרשת הרשאת עריכה כי אנחנו מצרפים מסמך לתיק\n if (!$this->acl->checkEntityEdit($case)) {\n $this->log->warning(\n \"LegalAssistance: ACL denied — user {$this->user->getId()} \" .\n \"attempted to attach document to case {$caseId}\"\n );\n throw new Forbidden(\"אין לך הרשאה לצרף מסמכים לתיק זה.\");\n }\n\n // ... המשך הקוד הקיים\n}\n```\n\n### 3. שיקולי מימוש\n\n- **למה `checkEntityEdit` ולא `checkEntityRead`?** — יצירת Document וקישורו לתיק היא פעולת כתיבה, לא קריאה. משתמש עם read-only על התיק לא צריך לוכל לשנות את רשימת המסמכים שלו.\n\n- **למה `$this->acl` ולא `AclManager`?** — ה-`Acl` service כבר מוזרק עם ה-User הנכון (המשתמש המבצע את הבקשה). שימוש ב-`AclManager->checkEntity($user, $entity, 'edit')` היה דורש לשלוף את ה-User לפי `$userId` — מיותר כש-DI כבר מספק לנו את זה.\n\n- **לוגים:** חשוב לתעד ניסיונות גישה שנדחו לצורך אבחון וניטור אבטחה.\n\n- **שגיאה בעברית:** ההודעה מוחזרת למשתמש דרך shira-hermes, אז עברית מתאימה לחוויית משתמש.\n\n### 4. קובץ יחיד לעריכה\n\n`files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/DirectAccessReportGenerator.php`\n\n**שורות לעריכה:**\n- שורות 12-19: הוספת `use` statements\n- שורות 26-31: הוספת `Acl` לקונסטרקטור\n- שורות 40-44: הוספת בדיקת ACL אחרי שליפת התיק", + "testStrategy": "## אסטרטגיית בדיקה\n\n### 1. בדיקה ידנית — מניעת IDOR\n\n**הכנה:**\n1. צור שני משתמשים ב-EspoCRM:\n - `lawyer-a` — עם תיק משויך (נקרא לו `case-a`)\n - `lawyer-b` — ללא גישה ל-`case-a`\n\n2. וודא ש-`lawyer-b` יכול לקרוא תיקים (scope-level read) אבל לא את `case-a` (record-level)\n\n**בדיקה:**\n1. התחבר כ-`lawyer-b`\n2. קרא ל-API:\n ```bash\n curl -X POST https://espocrm.dev.marcus-law.co.il/api/v1/SmartAssistant/action/executeTool \\\n -H \"Content-Type: application/json\" \\\n -H \"X-Api-Key: \" \\\n -d '{\n \"tool\": \"generate_direct_access_report\",\n \"caseId\": \"\",\n \"params\": {\"aidType\": \"יעוץ והדרכה\"}\n }'\n ```\n3. **תוצאה צפויה:** שגיאת 403 Forbidden עם הודעה \"אין לך הרשאה לצרף מסמכים לתיק זה.\"\n4. וודא שלא נוצר Document חדש ב-`case-a`\n\n### 2. בדיקת regression — פעולה תקינה\n\n1. התחבר כ-`lawyer-a` (בעל התיק)\n2. קרא לאותו API עם `caseId` של התיק שלו\n3. **תוצאה צפויה:** הדוח נוצר בהצלחה ומצורף לתיק\n\n### 3. בדיקת לוגים\n\n1. לאחר ניסיון IDOR שנדחה, בדוק בלוג:\n ```bash\n docker exec tail -20 data/logs/espo-$(date +%Y-%m-%d).log | grep \"ACL denied\"\n ```\n2. **תוצאה צפויה:** שורת לוג עם WARNING המציינת את user ID וה-case ID\n\n### 4. בדיקת אינטגרציה עם shira-hermes\n\n1. דרך ממשק Shira (צ'אט), בקש מעוזר המשפט ליצור דוח לתיק שאין לך גישה אליו\n2. **תוצאה צפויה:** הודעת שגיאה ידידותית בעברית מוחזרת למשתמש\n\n### 5. בדיקת unit (אופציונלי)\n\nאם קיימת תשתית PHPUnit בפרויקט:\n\n```php\npublic function testExecuteThrowsForbiddenWhenUserCannotEditCase(): void\n{\n // Mock Acl to return false for checkEntityEdit\n $acl = $this->createMock(Acl::class);\n $acl->method('checkEntityEdit')->willReturn(false);\n \n $generator = new DirectAccessReportGenerator(\n $this->entityManager,\n $this->injectableFactory,\n $this->log,\n $this->user,\n $acl\n );\n \n $this->expectException(Forbidden::class);\n $generator->execute(['aidType' => 'test'], 'some-case-id', 'user-id');\n}\n```", + "status": "pending", + "dependencies": [ + 1 + ], + "priority": "high", + "subtasks": [] + }, + { + "id": 4, + "title": "feat: P7 — שיוך דוח גישה ישירה לתיקיית CaseFiles", + "description": "הוספת ענף CaseFiles ליצירת המסמכים: אם CaseFilesCore מותקן (soft-detect), המסמך שנוצר משויך לתיקיית 'מסמכים' של התיק (folderId) כדי שיופיע בפאנל CaseDocs. ההתנהגות הקיימת (קישור לתיק + העלאה ל-NetworkStorage אם פעיל) נשמרת — ללא רגרסיה.", + "details": "אומת על dev 21/06/2026 (P7_OK): php -l נקי; קישור דרך Case.documents נראה דרך Document.cases (אותה טבלת קישור), folderId נקבע לתיקיית 'מסמכים'. ענף זהה לנתב P5/P6 המאומת.", + "testStrategy": "dev: יצירת מסמך עם CaseFilesCore מותקן → folderId=מסמכים, נראה בפאנל.", + "status": "done", + "dependencies": [], + "priority": "medium", + "subtasks": [] } ], "metadata": { "version": "1.0.0", - "lastModified": "2026-06-03T07:30:42.899Z", + "lastModified": "2026-06-03T08:19:08.136Z", "taskCount": 1, - "completedCount": 0, + "completedCount": 1, "tags": [ "master" - ] + ], + "created": "2026-06-20T12:07:53.783Z", + "description": "Tasks for master context", + "updated": "2026-06-21" } } } \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..2be3987 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,34 @@ +# LegalAssistance — Architecture (developer reference) + +> Internal developer doc. Not shipped to clients. Customer overview: `README.md`. Security/dead-code: `_AUDIT/LegalAssistance/findings.md`. + +**Module:** `LegalAssistance` · **Load order:** 25 · **Version:** 2.0.10 · **Author:** Marcus-Law · **Requires:** EspoCRM ≥ 8.0.0 + +## What it is +Generates the Israeli legal-aid "**דוח גישה ישירה**" (direct-access report) as a DOCX, straight from JSON params (no intermediate entity), and uploads it to the case folder. It is reached as a **SmartAssistant agent tool** — Shira runs an 8-step Hebrew interview to collect the data, then calls the tool. Since Phase 3, the legal tools live in **shira-hermes**, not ai-gateway; this extension is now minimal. + +## File-by-file +| File | Purpose | +|---|---| +| `files/.../SmartAssistant/Tools/DirectAccessReportGenerator.php` | **Core:** JSON params → DOCX via PHPWord `TemplateProcessor` (checkbox mapping) → upload to case folder via NetworkStorageIntegration (local fallback). **IDOR via caseId — S1; param→DOCX injection — S3; temp file — S4.** | +| `files/.../Resources/module.json` | Module order 25. | +| `files/.../Resources/metadata/app/smartAssistant.json` | Registers the `generate_direct_access_report` tool handler. | +| `files/.../Resources/metadata/entityDefs/Case.json` | **Empty `{}`** (no Case extension in v2.0.10) — D5. | +| `files/.../Resources/i18n/{en_US,he_IL,fa_IR}/Case.json` | Light "LegalAssistance" tab labels. (`Global.json` empty — D3.) | +| `plugins/legal-assistance/tools.json` | Shira tool schema for `generate_direct_access_report` (50+ params). | +| `plugins/legal-assistance/prompts.json` | Hebrew `casePromptAppend` — the 8-step interview flow. | +| `templates/direct-access-report.docx` | The DOCX template (39 KB binary). | +| `scripts/AfterInstall.php` | Creates the DocumentTemplate, clears cache, ensures i18n dirs. **ai-gateway deploy removed (Phase 3) — only a doc-comment remains.** | +| `scripts/AfterUninstall.php` | ⚠️ **Drops phantom v1.x tables + Case columns + i18n — destructive + dead (S2/D1).** | + +### Dead/reference clutter (not packaged, but in the repo — D2/D4) +`docs/examples/entity-reference-legalaid.md` (59 KB, removed architecture), `field_mapping.md`, `legal-docx-v3-fixed/` (Node.js reference), `.env.example`, `טופס`, old zips v1.0.0–1.3.2. + +## Dependencies +- **NetworkStorageIntegration (strong, graceful):** report upload to the case folder; falls back to local attachment storage if disabled. +- **SmartAssistant (hard, runtime):** the tool is only reachable via SmartAssistant's `executeTool` — it inherits SmartAssistant's IDOR (S1). +- **shira-hermes (soft):** Shira loads the `plugins/` tool + prompt to drive the interview. + +## Post-install gotchas +- Requires SmartAssistant + Shira configured; NetworkStorageIntegration recommended for case-folder filing. +- Fix the destructive `AfterUninstall` (S2) before any external release — it can wipe legal-aid data. diff --git a/README.md b/README.md new file mode 100644 index 0000000..31a1dbb --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# LegalAssistance — סיוע משפטי (דוח גישה ישירה) + +**גרסה:** 2.0.10 | **מחבר:** Marcus-Law | **EspoCRM:** >= 8.0.0 + +## תיאור + +יצירת **דוח גישה ישירה** (סיוע משפטי) כקובץ Word, ישירות מתוך שיחה עם העוזר **שירה**. שירה מנהל ראיון מובנה +(8 שלבים) לאיסוף הנתונים, ואז המערכת מייצרת את הדוח מתבנית DOCX וצורפת אותו לתיקיית התיק. + +## תלויות + +- **SmartAssistant + שירה (shira-hermes)** — נדרש; הכלי נקרא דרך מנגנון הכלים של שירה. +- **NetworkStorageIntegration** — מומלץ; שמירת הדוח בתיקיית התיק (יש fallback לאחסון מקומי). + +## התקנה + +1. ודא ש‑SmartAssistant ושירה מוגדרים. +2. הורד את `LegalAssistance` והתקן דרך **ניהול → הרחבות**. +3. בצע Rebuild ורענון קשיח. + +## מה מתווסף למערכת + +- כלי **"יצירת דוח גישה ישירה"** הזמין לעוזר שירה במצב תיק. +- תבנית מסמך דוח גישה ישירה. + +## שימוש + +במצב תיק, בקש משירה ליצור דוח גישה ישירה. שירה יאסוף את הפרטים בשלבים, ובסיום ייצור את ה‑DOCX וישמור אותו בתיק. + +## טיפול בעיות נפוצות + +| תסמין | סיבה | פתרון | +|---|---|---| +| הדוח לא נוצר | שירה/SmartAssistant לא מוגדרים | ודא שהאינטגרציה פעילה. | +| הדוח לא נשמר בתיק | NetworkStorageIntegration לא מותקן | יישמר מקומית; להתקנת NSI לשמירה בתיקיית התיק. | + +--- + +> תיעוד טכני למפתחים: `ARCHITECTURE.md`. diff --git a/files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/DirectAccessReportGenerator.php b/files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/DirectAccessReportGenerator.php index 4fc0b53..6bda35c 100644 --- a/files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/DirectAccessReportGenerator.php +++ b/files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/DirectAccessReportGenerator.php @@ -144,6 +144,20 @@ class DirectAccessReportGenerator ->getRelation($case, 'documents') ->relate($document); + // CaseFiles backend: place the Document in the case "מסמכים" folder so it + // appears in the CaseDocs panel tree (soft dependency, no hard requirement). + $caseFilesServiceClass = 'Espo\\Modules\\CaseFilesCore\\Services\\CaseFolderService'; + if (class_exists($caseFilesServiceClass)) { + try { + $cfFolder = $this->injectableFactory->create($caseFilesServiceClass) + ->getOrCreateSubfolder('Case', $caseId, 'מסמכים'); + $document->set('folderId', $cfFolder->getId()); + $this->entityManager->saveEntity($document, ['skipNetworkUpload' => true, 'silent' => true]); + } catch (\Throwable $e) { + $this->log->warning('LegalAssistance: CaseFiles folder placement failed: ' . $e->getMessage()); + } + } + // Upload to the case folder; align DB names to the canonical stored name. try { $nds = $this->injectableFactory->create(NetworkDocumentService::class);