Compare commits

...

2 Commits

Author SHA1 Message Date
chaim e7d901be29 chore: bump version to 2.1.0 (CaseFiles dual-backend integration — P5/P6/P7)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:15:47 +00:00
chaim 3178904b28 feat: place generated report in the CaseFiles case folder (P7)
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) <noreply@anthropic.com>
2026-06-21 10:53:23 +00:00
5 changed files with 141 additions and 10 deletions
+46 -6
View File
@@ -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\n<?php\n/************************************************************************\n * LegalAssistance Extension — AfterUninstall\n *\n * 1. Remove DocumentTemplate records created by this extension\n * 2. Remove custom i18n files\n * 3. (Optional) Remove layout references to extension fields\n * 4. Clear cache\n *\n * Note: Database tables and columns are intentionally preserved to\n * protect customer data during uninstall/reinstall cycles (rule N2).\n ************************************************************************/\n\nclass AfterUninstall\n{\n public function run(Container $container): void\n {\n // ... (שמירה על removeDocumentTemplates, removeCustomI18n, clearCache)\n // הסרה מלאה של dropTables ו-cleanCaseColumns\n }\n}\n```",
"testStrategy": "## אסטרטגיית בדיקה\n\n### 1. בדיקת יחידה - סקריפט האנאינסטול\n- וודא שהקובץ `AfterUninstall.php` לא מכיל את המילים `DROP TABLE` או `DROP COLUMN`\n- וודא שאין קריאות לפונקציות `dropTables` או `cleanCaseColumns`\n\n### 2. בדיקת אינטגרציה בסביבת פיתוח\n1. **הכנה**: התקן את ההרחבה על instance ריק של EspoCRM\n2. **יצירת נתונים**: צור Case עם ערכים בשדות הרלוונטיים (אם קיימים)\n3. **הסרה**: הסר את ההרחבה דרך Administration → Extensions\n4. **אימות**: בדוק שהטבלאות `legal_aid` ו-`direct_access_report` עדיין קיימות (אם היו מלכתחילה)\n5. **אימות**: בדוק שהעמודות ב-Case עדיין קיימות (אם היו מלכתחילה)\n6. **התקנה מחדש**: התקן את ההרחבה שוב\n7. **אימות**: וודא שהנתונים שנשמרו בשלב 2 עדיין קיימים\n\n### 3. בדיקת לוגים\n- לאחר הסרה, בדוק את `data/logs/espo-YYYY-MM-DD.log`\n- וודא שאין שורות `Dropping table` או `Dropped column`\n- וודא שיש שורות `Removed DocumentTemplate` ו-`Cache cleared`\n\n### 4. בדיקת קוד סטטית\n```bash\n# בדוק שאין DROP statements\ngrep -n \"DROP TABLE\\|DROP COLUMN\" scripts/AfterUninstall.php\n# צפוי: אין תוצאות\n\n# בדוק שאין קריאות לפונקציות שהוסרו\ngrep -n \"dropTables\\|cleanCaseColumns\" scripts/AfterUninstall.php\n# צפוי: אין תוצאות\n```\n\n### 5. בדיקת רגרסיה\n- וודא שה-DocumentTemplate עדיין נמחק כראוי\n- וודא שקבצי i18n עדיין נמחקים כראוי\n- וודא ש-cache clearing עדיין עובד",
"status": "pending",
"dependencies": [
1
],
"priority": "high",
"subtasks": []
},
{
"id": 3,
"title": "תיקון אבטחה: הוספת בדיקת ACL ברמת רשומה ב-DirectAccessReportGenerator",
"description": "הוספת בדיקת הרשאות ברמת רשומה (record-level ACL) לכלי generate_direct_access_report למניעת IDOR — כרגע הכלי סומך רק על בדיקת scope-level ומאפשר למשתמש עם הרשאת קריאה לתיקים לצרף מסמכים לכל תיק במערכת.",
"details": "## רקע\n\nהממצא F-001 בביקורת האבטחה (SECURITY_AUDIT_2026-04-25.md:61-134) מזהה פגיעות IDOR חמורה:\n\nהכלי `DirectAccessReportGenerator.php` מקבל `caseId` שרירותי ומייצר מסמך DOCX שמצורף לתיק ללא בדיקת הרשאות ברמת הרשומה. הבדיקה היחידה שמתבצעת היא ב-upstream (SmartAssistant ActionExecutor) ברמת scope בלבד (`acl->checkScope('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: <lawyer-b-api-key>\" \\\n -d '{\n \"tool\": \"generate_direct_access_report\",\n \"caseId\": \"<case-a-id>\",\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 <espocrm-container> 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"
}
}
}
+34
View File
@@ -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.01.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.
+39
View File
@@ -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`.
@@ -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);
+8 -4
View File
@@ -1,9 +1,13 @@
{
"name": "LegalAssistance",
"version": "2.0.10",
"acceptableVersions": [">=8.0.0"],
"php": [">=8.1"],
"releaseDate": "2026-06-03",
"version": "2.1.0",
"acceptableVersions": [
">=8.0.0"
],
"php": [
">=8.1"
],
"releaseDate": "2026-06-21",
"author": "Marcus-Law",
"description": "סיוע משפטי — יצירת דוח גישה ישירה (JSON ישירות ל-DOCX), אינטגרציה עם שירה",
"scripts": {