diff --git a/Admin/Install/Media.install.json b/Admin/Install/Media.install.json index d9d8638..5f3875f 100755 --- a/Admin/Install/Media.install.json +++ b/Admin/Install/Media.install.json @@ -13,47 +13,164 @@ "virtualPath": "/Modules/Admin", "user": 1 }, + { + "type": "collection", + "create_directory": true, + "name": "Global", + "virtualPath": "/Modules/Admin/Templates", + "user": 1 + }, + { + "type": "collection", + "create_directory": true, + "name": "Helper", + "virtualPath": "/Modules/Admin/Templates/Global", + "user": 1 + }, + { + "type": "collection", + "create_directory": true, + "name": "Lists", + "virtualPath": "/Modules/Admin/Templates/Global", + "user": 1 + }, + { + "type": "collection", + "create_directory": true, + "name": "Letters", + "virtualPath": "/Modules/Admin/Templates/Global", + "user": 1 + }, + { + "type": "collection", + "create_directory": true, + "name": "Emails", + "virtualPath": "/Modules/Admin/Templates/Global", + "user": 1 + }, + { + "type": "collection", + "create_directory": true, + "name": "Data", + "virtualPath": "/Modules/Admin/Templates/Global", + "user": 1 + }, + { + "type": "collection", + "create_directory": true, + "name": "Reports", + "virtualPath": "/Modules/Admin/Templates/Global", + "user": 1 + }, { "type": "upload", "create_collection": true, - "name": "Pdf Exporter", - "virtualPath": "/Modules/Admin/Templates", - "path": "/Modules/Admin/Templates/Pdf Exporter", + "name": "Assets", + "virtualPath": "/Modules/Admin/Templates/Global/Helper", + "path": "/Modules/Admin/Templates/Global/Helper/Assets", "files": [ - "/Modules/Admin/Admin/Install/Media/PdfExporter" + "/Modules/Admin/Admin/Install/Media/Assets" ], "user": 1 }, { "type": "upload", "create_collection": true, - "name": "Excel Exporter", - "virtualPath": "/Modules/Admin/Templates", - "path": "/Modules/Admin/Templates/Excel Exporter", + "name": "Pdf Default Template", + "virtualPath": "/Modules/Admin/Templates/Global/Helper", + "path": "/Modules/Admin/Templates/Global/Helper/Pdf Default Template", "files": [ - "/Modules/Admin/Admin/Install/Media/ExcelExporter" + "/Modules/Admin/Admin/Install/Media/PdfDefaultTemplate" ], "user": 1 }, { "type": "upload", "create_collection": true, - "name": "Csv Exporter", - "virtualPath": "/Modules/Admin/Templates", - "path": "/Modules/Admin/Templates/Csv Exporter", + "name": "Word Default Template", + "virtualPath": "/Modules/Admin/Templates/Global/Helper", + "path": "/Modules/Admin/Templates/Global/Helper/Word Default Template", "files": [ - "/Modules/Admin/Admin/Install/Media/CsvExporter" + "/Modules/Admin/Admin/Install/Media/WordDefaultTemplate" ], "user": 1 }, { "type": "upload", "create_collection": true, - "name": "Word Exporter", - "virtualPath": "/Modules/Admin/Templates", - "path": "/Modules/Admin/Templates/Word Exporter", + "name": "Word Plain Template", + "virtualPath": "/Modules/Admin/Templates/Global/Helper", + "path": "/Modules/Admin/Templates/Global/Helper/Word Plain Template", "files": [ - "/Modules/Admin/Admin/Install/Media/WordExporter" + "/Modules/Admin/Admin/Install/Media/WordPlainTemplate" + ], + "user": 1 + }, + { + "type": "upload", + "create_collection": true, + "name": "Excel Default Template", + "virtualPath": "/Modules/Admin/Templates/Global/Helper", + "path": "/Modules/Admin/Templates/Global/Helper/Excel Default Template", + "files": [ + "/Modules/Admin/Admin/Install/Media/ExcelDefaultTemplate" + ], + "user": 1 + }, + + { + "type": "upload", + "create_collection": true, + "name": "Pdf List Exporter", + "virtualPath": "/Modules/Admin/Templates/Global/Lists", + "path": "/Modules/Admin/Templates/Global/Lists/Pdf List Exporter", + "files": [ + "/Modules/Admin/Admin/Install/Media/PdfListExporter" + ], + "user": 1 + }, + { + "type": "upload", + "create_collection": true, + "name": "Word List Exporter", + "virtualPath": "/Modules/Admin/Templates/Global/Lists", + "path": "/Modules/Admin/Templates/Global/Lists/Word List Exporter", + "files": [ + "/Modules/Admin/Admin/Install/Media/WordListExporter" + ], + "user": 1 + }, + { + "type": "upload", + "create_collection": true, + "name": "Excel List Exporter", + "virtualPath": "/Modules/Admin/Templates/Global/Lists", + "path": "/Modules/Admin/Templates/Global/Lists/Excel List Exporter", + "files": [ + "/Modules/Admin/Admin/Install/Media/ExcelListExporter" + ], + "user": 1 + }, + { + "type": "upload", + "create_collection": true, + "name": "Csv List Exporter", + "virtualPath": "/Modules/Admin/Templates/Global/Lists", + "path": "/Modules/Admin/Templates/Global/Lists/Csv List Exporter", + "files": [ + "/Modules/Admin/Admin/Install/Media/CsvListExporter" + ], + "user": 1 + }, + + { + "type": "upload", + "create_collection": true, + "name": "Word List Exporter", + "virtualPath": "/Modules/Admin/Templates/Global/Letters", + "path": "/Modules/Admin/Templates/Global/Letters/Word Letter Exporter", + "files": [ + "/Modules/Admin/Admin/Install/Media/WordLetterExporter" ], "user": 1 }, @@ -61,11 +178,71 @@ "type": "upload", "create_collection": true, "name": "Email Exporter", - "virtualPath": "/Modules/Admin/Templates", - "path": "/Modules/Admin/Templates/Email Exporter", + "virtualPath": "/Modules/Admin/Templates/Global/Emails", + "path": "/Modules/Admin/Templates/Global/Emails/Email Exporter", "files": [ "/Modules/Admin/Admin/Install/Media/EmailExporter" ], "user": 1 + }, + + { + "type": "reference", + "name": "Assets", + "from": "/Modules/Admin/Templates/Global/Helper/Pdf Default Template", + "to": "/Modules/Admin/Templates/Global/Helper/Assets", + "user": 1 + }, + { + "type": "reference", + "name": "Assets", + "from": "/Modules/Admin/Templates/Global/Helper/Word Default Template", + "to": "/Modules/Admin/Templates/Global/Helper/Assets", + "user": 1 + }, + { + "type": "reference", + "name": "Assets", + "from": "/Modules/Admin/Templates/Global/Helper/Word Plain Template", + "to": "/Modules/Admin/Templates/Global/Helper/Assets", + "user": 1 + }, + + { + "type": "reference", + "name": "Assets", + "from": "/Modules/Admin/Templates/Global/Helper/Excel Default Template", + "to": "/Modules/Admin/Templates/Global/Helper/Assets", + "user": 1 + }, + + { + "type": "reference", + "name": "Helper", + "from": "/Modules/Admin/Templates/Global/Lists/Pdf List Exporter", + "to": "/Modules/Admin/Templates/Global/Helper/Pdf Default Template", + "user": 1 + }, + { + "type": "reference", + "name": "Helper", + "from": "/Modules/Admin/Templates/Global/Lists/Word List Exporter", + "to": "/Modules/Admin/Templates/Global/Helper/Word Default Template", + "user": 1 + }, + { + "type": "reference", + "name": "Helper", + "from": "/Modules/Admin/Templates/Global/Lists/Excel List Exporter", + "to": "/Modules/Admin/Templates/Global/Helper/Excel Default Template", + "user": 1 + }, + + { + "type": "reference", + "name": "Helper", + "from": "/Modules/Admin/Templates/Global/Letters/Word Letter Exporter", + "to": "/Modules/Admin/Templates/Global/Helper/Word Default Template", + "user": 1 } ] \ No newline at end of file diff --git a/Admin/Install/Media.php b/Admin/Install/Media.php index 064a5b6..464f745 100755 --- a/Admin/Install/Media.php +++ b/Admin/Install/Media.php @@ -43,26 +43,26 @@ class Media { $media = \Modules\Media\Admin\Installer::installExternal($app, ['path' => __DIR__ . '/Media.install.json']); - $defaultPdfExport = (int) \reset($media['upload'][0]); - $defaultExcelExport = (int) \reset($media['upload'][1]); - $defaultCsvExport = (int) \reset($media['upload'][2]); - $defaultWordExport = (int) \reset($media['upload'][3]); - $defaultEmailExport = (int) \reset($media['upload'][4]); + SettingMapper::create()->execute( + new Setting( + 0, + SettingsEnum::DEFAULT_LIST_EXPORTS, + (string) $media['collection'][4]['id'], + '\\d+', + unit: 1, + module: 'Admin' + ) + ); SettingMapper::create()->execute( - new Setting(0, SettingsEnum::DEFAULT_PDF_EXPORT_TEMPLATE, (string) $defaultPdfExport, '\\d+', 1, 'Admin') - ); - SettingMapper::create()->execute( - new Setting(0, SettingsEnum::DEFAULT_EXCEL_EXPORT_TEMPLATE, (string) $defaultExcelExport, '\\d+', 1, 'Admin') - ); - SettingMapper::create()->execute( - new Setting(0, SettingsEnum::DEFAULT_CSV_EXPORT_TEMPLATE, (string) $defaultCsvExport, '\\d+', 1, 'Admin') - ); - SettingMapper::create()->execute( - new Setting(0, SettingsEnum::DEFAULT_WORD_EXPORT_TEMPLATE, (string) $defaultWordExport, '\\d+', 1, 'Admin') - ); - SettingMapper::create()->execute( - new Setting(0, SettingsEnum::DEFAULT_EMAIL_EXPORT_TEMPLATE, (string) $defaultEmailExport, '\\d+', 1, 'Admin') + new Setting( + 0, + SettingsEnum::DEFAULT_LETTERS, + (string) $media['collection'][5]['id'], + '\\d+', + unit: 1, + module: 'Admin' + ) ); } } diff --git a/Admin/Install/Media/CsvExporter/defaultCsvExporter.csv.php b/Admin/Install/Media/CsvListExporter/defaultCsvExporter.csv.php similarity index 59% rename from Admin/Install/Media/CsvExporter/defaultCsvExporter.csv.php rename to Admin/Install/Media/CsvListExporter/defaultCsvExporter.csv.php index 0da7105..2be87ca 100755 --- a/Admin/Install/Media/CsvExporter/defaultCsvExporter.csv.php +++ b/Admin/Install/Media/CsvListExporter/defaultCsvExporter.csv.php @@ -12,4 +12,12 @@ */ declare(strict_types=1); -$exporter = null; +$data = $this->getData('data') ?? []; + +$out = \fopen('php://output', 'w'); + +foreach ($data as $row) { + fputcsv($out, $row); +} + +\fclose($out); diff --git a/Admin/Install/Media/ExcelDefaultTemplate/template.php b/Admin/Install/Media/ExcelDefaultTemplate/template.php new file mode 100644 index 0000000..1479aa2 --- /dev/null +++ b/Admin/Install/Media/ExcelDefaultTemplate/template.php @@ -0,0 +1,46 @@ +getActiveSheet() + ->getPageSetup() + ->setPaperSize(PageSetup::PAPERSIZE_A4); + + $this->getActiveSheet() + ->getHeaderFooter() + ->setOddHeader("&L&B&20Jingga\n&B&10Business solutions made simple."); + + $this->getActiveSheet() + ->getHeaderFooter() + ->setOddFooter('&RPage &P/&N'); + + /* + Tested with LibreOffice and not working (requires &G above in the text). + Either it is broken or LibreOffice cannot show the image. + $drawing = new HeaderFooterDrawing(); + $drawing->setName('PhpSpreadsheet logo'); + $drawing->setPath(__DIR__ . '/../Web/Backend/img/logo.png'); + $drawing->setHeight(50); + + $this->getActiveSheet() + ->getHeaderFooter() + ->addImage($drawing, HeaderFooter::IMAGE_HEADER_LEFT); + */ + } + +} diff --git a/Admin/Install/Media/ExcelExporter/defaultExcelExporter.xls.php b/Admin/Install/Media/ExcelExporter/defaultExcelExporter.xls.php deleted file mode 100755 index 0da7105..0000000 --- a/Admin/Install/Media/ExcelExporter/defaultExcelExporter.xls.php +++ /dev/null @@ -1,15 +0,0 @@ -getData('media'); +$data = $this->getData('data') ?? []; + +include $media->getSourceByName('template.php')->getAbsolutePath(); + +$excel = new DefaultExcel(); + +foreach ($data as $i => $row) { + foreach ($row as $j => $cell) { + $excel->getActiveSheet()->setCellValueByColumnAndRow($j + 1, $i + 1, $cell); + } +} + +$file = \tempnam(\sys_get_temp_dir(), 'oms_'); + +$writer = IOFactory::createWriter($excel, 'Xlsx'); +$writer->save($file); + +echo \file_get_contents($file); + +\unlink($file); \ No newline at end of file diff --git a/Admin/Install/Media/PdfDefaultTemplate/template.php b/Admin/Install/Media/PdfDefaultTemplate/template.php new file mode 100644 index 0000000..2fc0ef7 --- /dev/null +++ b/Admin/Install/Media/PdfDefaultTemplate/template.php @@ -0,0 +1,137 @@ +header_xobjid === false) { + $this->header_xobjid = $this->startTemplate($this->w, 0); + + // Set Logo + $image_file = __DIR__ . '/../Web/Backend/img/logo.png'; + $this->Image($image_file, 15, 15, 15, 15, 'PNG', '', 'T', false, 300, '', false, false, 0, false, false, false); + + // Set Title + $this->SetFont('helvetica', 'B', 20); + $this->setX(15 + 15 + 3); + $this->Cell(0, 14, $this->header_title, 0, false, 'L', 0, '', 0, false, 'T', 'M'); + + $this->SetFont('helvetica', '', 10); + $this->setX(15 + 15 + 3); + $this->Cell(0, 26, $this->header_string, 0, false, 'L', 0, '', 0, false, 'T', 'M'); + + $this->endTemplate(); + } + + $x = 0; + $dx = 0; + + if (!$this->header_xobj_autoreset AND $this->booklet AND (($this->page % 2) == 0)) { + // adjust margins for booklet mode + $dx = ($this->original_lMargin - $this->original_rMargin); + } + + if ($this->rtl) { + $x = $this->w + $dx; + } else { + $x = 0 + $dx; + } + + $this->printTemplate($this->header_xobjid, $x, 0, 0, 0, '', '', false); + if ($this->header_xobj_autoreset) { + // reset header xobject template at each page + $this->header_xobjid = false; + } + } + + // Page footer + public function Footer() { + $this->SetY(-25); + + $this->SetFont('helvetica', 'I', 7); + $this->Cell($this->getPageWidth() - 22, 0, 'Page '.$this->getAliasNumPage().'/'.$this->getAliasNbPages(), 0, false, 'R', 0, '', 0, false, 'T', 'M'); + $this->Ln(); + $this->Ln(); + + $this->SetFillColor(245, 245, 245); + $this->SetX(0); + $this->Cell($this->getPageWidth(), 25, '', 0, 0, 'L', true, '', 0, false, 'T', 'T'); + + $this->SetFont('helvetica', '', 7); + $this->SetXY(15 + 10, -15, true); + $this->MultiCell(30, 0, "Jingga e.K.\nGartenstr. 26\n61206 Woellstadt", 0, 'L', false, 1, null, null, true, 0, false, true, 0, 'B'); + + $this->SetXY(25 + 15 + 20, -15, true); + $this->MultiCell(40, 0, "Geschäftsführer: Dennis Eichhorn\nFinanzamt: HRB ???\nUSt Id: DE ??????????", 0, 'L', false, 1, null, null, true, 0, false, true, 0, 'B'); + + $this->SetXY(25 + 45 + 15 + 30, -15, true); + $this->MultiCell(35, 0, "Volksbank Mittelhessen\nBIC: ??????????\nIBAN: ???????????", 0, 'L', false, 1, null, null, true, 0, false, true, 0, 'B'); + + $this->SetXY(25 + 45 + 35 + 15 + 40, -15, true); + $this->MultiCell(35, 0, "www.jingga.app\ninfo@jingga.app\n+49 0152 ???????", 0, 'L', false, 1, null, null, true, 0, false, true, 0, 'B'); + } + + public function __construct() + { + parent::__construct('P', 'mm', 'A4', true, 'UTF-8', false); + + $this->SetCreator("Jingga"); + + // set default header data + $this->SetHeaderData(PDF_HEADER_LOGO, PDF_HEADER_LOGO_WIDTH, 'Jingga', 'Business solutions made simple.'); + + // set header and footer fonts + $this->SetHeaderFont([PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN]); + $this->SetFooterFont([PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA]); + + // set default monospaced font + $this->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED); + + // set margins + $this->SetMargins(15, 30, 15); + + // set auto page breaks + $this->SetAutoPageBreak(true, 25); + + // set image scale factor + $this->SetImageScale(PDF_IMAGE_SCALE_RATIO); + + // add a page + $this->AddPage(); + } +} + +/* +[ + 'company' => '', + 'slogan' => '', + 'company_full' => '', + 'address' => '', + 'ciry' => '', + 'manager' => '', + 'tax_office' => '', + 'tax_id' => '', + 'tax_vat' => '', + 'bank_name' => '', + 'bank_bic' => '', + 'bank_iban' => '', + 'website' => '', + 'email' => '', + 'phone' => '', + 'creator' => '', + 'date' => '', +] +*/ diff --git a/Admin/Install/Media/PdfExporter/defaultPdfExporter.pdf.php b/Admin/Install/Media/PdfExporter/defaultPdfExporter.pdf.php deleted file mode 100755 index 0da7105..0000000 --- a/Admin/Install/Media/PdfExporter/defaultPdfExporter.pdf.php +++ /dev/null @@ -1,15 +0,0 @@ -getData('media'); +$data = $this->getData('data') ?? []; + +include $media->getSourceByName('template.php')->getAbsolutePath(); + +$excel = new DefaultPdf(); + +$topPos = $pdf->getY(); + +$tbl = ''; +foreach ($data as $i => $row) { + if ($i === 0) { + $tbl = ''; + + foreach ($row as $j => $cell) { + $tbl .= ''; + } + + $tbl .= ''; + } else { + $tbl .= ''; + foreach ($row as $j => $cell) { + $tbl .= ''; + } + $tbl .= ''; + } +} +$tbl .= '
' . $cell . '
' . $cell . '
'; + +$pdf->Output('list.pdf', 'I'); \ No newline at end of file diff --git a/Admin/Install/Media/WordDefaultTemplate/template.php b/Admin/Install/Media/WordDefaultTemplate/template.php new file mode 100644 index 0000000..51e3a14 --- /dev/null +++ b/Admin/Install/Media/WordDefaultTemplate/template.php @@ -0,0 +1,186 @@ + 100, 'bgColor' => 'f5f5f5']; + $this->addTableStyle('FooterTableStyle', $generalTableStyle); + } + + public function createFirstPage() + { + $section = $this->addSection([ + 'marginLeft' => 1000, + 'marginRight' => 1000, + 'marginTop' => 2000, + 'marginBottom' => 2000, + // 'headerHeight' => 50, + // 'footerHeight' => 50, + ]); + + // first page header + $firstHeader = $section->addHeader(); + $firstHeader->firstPage(); + + $table = $firstHeader->addTable(); + $table->addRow(); + + // first column + $table->addCell(1300)->addImage(__DIR__ . '/../Web/Backend/img/logo.png', ['width' => 50, 'height' => 50]); + + //second column + $cell = $table->addCell(8700, ['valign' => 'bottom']); + $textrun = $cell->addTextRun(); + $textrun->addText('Jingga', ['name' => 'helvetica', 'bold' => true, 'size' => 20]); + + $textrun = $cell->addTextRun(); + $textrun->addText('Business solutions made simple.', ['name' => 'helvetica', 'size' => 10]); + + // first page footer + $firstFooter = $section->addFooter(); + $firstFooter->firstPage(); + $firstFooter->addPreserveText('Page {PAGE}/{NUMPAGES}', ['name' => 'helvetica', 'italic' => true], ['alignment' => \PhpOffice\PhpWord\SimpleType\Jc::END]); + $firstFooter->addTextRun(); + + $table = $firstFooter->addTable('FooterTableStyle'); + $table->addRow(); + + // columns + $cell = $table->addCell(500); + + $cell = $table->addCell(2000); + $textrun = $cell->addTextRun(); + $textrun->addText('Jingga e.K.', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('Gartenstr. 26', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('61206 Woellstadt', ['name' => 'helvetica', 'size' => 7]); + + $cell = $table->addCell(2700); + $textrun = $cell->addTextRun(); + $textrun->addText('Geschäftsführer: Dennis Eichhorn', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('Finanzamt: HRB ???', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('USt Id: DE ??????????', ['name' => 'helvetica', 'size' => 7]); + + $cell = $table->addCell(2700); + $textrun = $cell->addTextRun(); + $textrun->addText('Volksbank Mittelhessen', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('BIC: ??????????', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('IBAN: ???????????', ['name' => 'helvetica', 'size' => 7]); + + $cell = $table->addCell(2100); + $textrun = $cell->addTextRun(); + $textrun->addText('www.jingga.app', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('info@jingga.app', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('+49 0152 ???????', ['name' => 'helvetica', 'size' => 7]); + + return $section; + } + + public function createSecondPage() + { + $section = $this->addSection([ + 'marginLeft' => 1000, + 'marginRight' => 1000, + 'marginTop' => 2000, + 'marginBottom' => 2000, + // 'headerHeight' => 50, + // 'footerHeight' => 50, + ]); + + $header = $section->addHeader(); + $table = $header->addTable(); + $table->addRow(); + + // first column + $table->addCell(1300)->addImage(__DIR__ . '/../Web/Backend/img/logo.png', ['width' => 50, 'height' => 50]); + + //second column + $cell = $table->addCell(8700, ['valign' => 'bottom']); + $textrun = $cell->addTextRun(); + $textrun->addText('Jingga', ['name' => 'helvetica', 'bold' => true, 'size' => 20]); + + $textrun = $cell->addTextRun(); + $textrun->addText('Business solutions made simple.', ['name' => 'helvetica', 'size' => 10]); + + $footer = $section->addFooter(); + $footer->addPreserveText('Page {PAGE}/{NUMPAGES}', ['name' => 'helvetica', 'italic' => true], ['alignment' => \PhpOffice\PhpWord\SimpleType\Jc::END]); + $footer->addTextRun(); + + $table = $footer->addTable('FooterTableStyle'); + $table->addRow(); + + // columns + $cell = $table->addCell(500); + + $cell = $table->addCell(2000); + $textrun = $cell->addTextRun(); + $textrun->addText('Jingga e.K.', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('Gartenstr. 26', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('61206 Woellstadt', ['name' => 'helvetica', 'size' => 7]); + + $cell = $table->addCell(2700); + $textrun = $cell->addTextRun(); + $textrun->addText('Geschäftsführer: Dennis Eichhorn', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('Finanzamt: HRB ???', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('USt Id: DE ??????????', ['name' => 'helvetica', 'size' => 7]); + + $cell = $table->addCell(2700); + $textrun = $cell->addTextRun(); + $textrun->addText('Volksbank Mittelhessen', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('BIC: ??????????', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('IBAN: ???????????', ['name' => 'helvetica', 'size' => 7]); + + $cell = $table->addCell(2100); + $textrun = $cell->addTextRun(); + $textrun->addText('www.jingga.app', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('info@jingga.app', ['name' => 'helvetica', 'size' => 7]); + $textrun = $cell->addTextRun(); + $textrun->addText('+49 0152 ???????', ['name' => 'helvetica', 'size' => 7]); + + return $section; + } +} + +/* +[ + 'company' => '', + 'slogan' => '', + 'company_full' => '', + 'address' => '', + 'ciry' => '', + 'manager' => '', + 'tax_office' => '', + 'tax_id' => '', + 'tax_vat' => '', + 'bank_name' => '', + 'bank_bic' => '', + 'bank_iban' => '', + 'website' => '', + 'email' => '', + 'phone' => '', + 'creator' => '', + 'date' => '', +] +*/ diff --git a/Admin/Install/Media/WordExporter/defaultWordExporter.doc.php b/Admin/Install/Media/WordExporter/defaultWordExporter.doc.php deleted file mode 100755 index 0da7105..0000000 --- a/Admin/Install/Media/WordExporter/defaultWordExporter.doc.php +++ /dev/null @@ -1,15 +0,0 @@ -getData('media'); +$data = $this->getData('data') ?? []; + +include $media->getSourceByName('template.php')->getAbsolutePath(); + +$word = new DefaultWord(); +$section = $word->createFirstPage(); + +$file = \tempnam(\sys_get_temp_dir(), 'oms_'); +$writer->save($file); + +echo \file_get_contents($file); + +\unlink($file); \ No newline at end of file diff --git a/Admin/Install/Media/WordListExporter/defaultWordListExporter.doc.php b/Admin/Install/Media/WordListExporter/defaultWordListExporter.doc.php new file mode 100755 index 0000000..e2ece94 --- /dev/null +++ b/Admin/Install/Media/WordListExporter/defaultWordListExporter.doc.php @@ -0,0 +1,55 @@ +getData('media'); +$data = $this->getData('data') ?? []; + +include $media->getSourceByName('template.php')->getAbsolutePath(); + +$word = new DefaultWord(); +$section = $word->createFirstPage(); + +$tbl = ''; +foreach ($data as $i => $row) { + if ($i === 0) { + $tbl = ''; + + foreach ($row as $j => $cell) { + $tbl .= ''; + } + + $tbl .= ''; + } else { + $tbl .= ''; + foreach ($row as $j => $cell) { + $tbl .= ''; + } + $tbl .= ''; + } +} +$tbl .= '
' . $cell . '
' . $cell . '
'; + +\PhpOffice\PhpWord\Shared\Html::addHtml($section, $tbl, false, false); + +$file = \tempnam(\sys_get_temp_dir(), 'oms_'); +$writer->save($file); + +echo \file_get_contents($file); + +\unlink($file); \ No newline at end of file diff --git a/Admin/Install/Media/WordPlainTemplate/defaultWordExporter.doc.php b/Admin/Install/Media/WordPlainTemplate/defaultWordExporter.doc.php new file mode 100644 index 0000000..c79b819 --- /dev/null +++ b/Admin/Install/Media/WordPlainTemplate/defaultWordExporter.doc.php @@ -0,0 +1,33 @@ +getData('media'); +$data = $this->getData('data') ?? []; + +include $media->getSourceByName('template.php')->getAbsolutePath(); + +$word = new DefaultWord(); +$section = $word->createFirstPage(); + +$file = \tempnam(\sys_get_temp_dir(), 'oms_'); +$writer->save($file); + +echo \file_get_contents($file); + +\unlink($file); \ No newline at end of file diff --git a/Admin/Install/db.json b/Admin/Install/db.json index ceb61b2..986774d 100755 --- a/Admin/Install/db.json +++ b/Admin/Install/db.json @@ -507,6 +507,56 @@ } } }, + "unit": { + "name": "unit", + "fields": { + "unit_id": { + "name": "unit_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "unit_name": { + "name": "unit_name", + "type": "VARCHAR(50)", + "default": null, + "null": true + }, + "unit_image": { + "name": "unit_image", + "type": "INT", + "default": null, + "null": true + }, + "unit_description": { + "name": "unit_description", + "type": "TEXT", + "default": null, + "null": true + }, + "unit_descriptionraw": { + "name": "unit_descriptionraw", + "type": "TEXT", + "default": null, + "null": true + }, + "unit_parent": { + "name": "unit_parent", + "type": "INT", + "default": null, + "null": true, + "foreignTable": "unit", + "foreignKey": "unit_id" + }, + "unit_status": { + "name": "unit_status", + "type": "TINYINT", + "default": null, + "null": true + } + } + }, "app": { "name": "app", "fields": { @@ -553,7 +603,8 @@ "group_name": { "name": "group_name", "type": "VARCHAR(50)", - "null": false + "null": false, + "unique": true }, "group_status": { "name": "group_status", @@ -601,9 +652,12 @@ "name": "group_permission_unit", "type": "INT", "default": null, - "null": true + "null": true, + "foreignTable": "unit", + "foreignKey": "unit_id" }, "group_permission_app": { + "description": "@todo: consider to use int as value and create foreign key", "name": "group_permission_app", "type": "VARCHAR(255)", "default": null, @@ -681,6 +735,11 @@ "primary": true, "autoincrement": true }, + "account_id_temp": { + "name": "account_id_temp", + "type": "VARCHAR(65)", + "null": false + }, "account_status": { "name": "account_status", "type": "TINYINT", @@ -748,6 +807,14 @@ "gdpr": true } }, + "account_email_temp": { + "name": "account_email_temp", + "type": "VARCHAR(70)", + "null": false, + "annotations": { + "gdpr": true + } + }, "account_tries": { "name": "account_tries", "type": "TINYINT", @@ -775,6 +842,87 @@ } } }, + "account_address_rel": { + "name": "account_address_rel", + "fields": { + "account_address_rel_id": { + "name": "account_address_rel_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "account_address_rel_contact": { + "name": "account_address_rel_contact", + "type": "INT", + "null": false, + "foreignTable": "profile_contact", + "foreignKey": "profile_contact_id" + }, + "account_address_rel_module": { + "name": "account_address_rel_module", + "type": "INT", + "null": true, + "default": null, + "foreignTable": "module", + "foreignKey": "module_id" + }, + "account_address_rel_address": { + "name": "account_address_rel_address", + "type": "INT", + "null": false, + "foreignTable": "address", + "foreignKey": "address_id" + } + } + }, + "account_contact": { + "name": "account_contact", + "fields": { + "account_contact_id": { + "name": "account_contact_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "account_contact_type": { + "name": "account_contact_type", + "type": "TINYINT", + "null": false + }, + "account_contact_subtype": { + "name": "account_contact_subtype", + "type": "TINYINT", + "null": false + }, + "account_contact_order": { + "name": "account_contact_order", + "type": "INT", + "null": false + }, + "account_contact_content": { + "name": "account_contact_content", + "type": "VARCHAR(255)", + "null": false + }, + "account_contact_module": { + "name": "account_contact_module", + "type": "INT", + "null": true, + "default": null, + "foreignTable": "module", + "foreignKey": "module_id" + }, + "account_contact_account": { + "name": "account_contact_account", + "type": "INT", + "null": false, + "foreignTable": "account", + "foreignKey": "account_id" + } + } + }, "account_account_rel": { "description": "Accounts can belong to other accounts. E.g. a user can belong to a company account", "name": "account_account_rel", @@ -919,6 +1067,41 @@ } } }, + "account_api": { + "name": "account_api", + "fields": { + "account_api_id": { + "name": "account_api_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "account_api_status": { + "name": "account_api_status", + "type": "TINYINT", + "null": false + }, + "account_api_key": { + "name": "account_api_key", + "type": "VARCHAR(129)", + "null": false, + "unique": true + }, + "account_api_created_at": { + "name": "account_api_created_at", + "type": "DATETIME", + "null": false + }, + "account_api_account": { + "name": "account_api_account", + "type": "INT", + "null": false, + "foreignTable": "account", + "foreignKey": "account_id" + } + } + }, "settings": { "name": "settings", "fields": { @@ -944,6 +1127,14 @@ "type": "TEXT", "null": true }, + "settings_unit": { + "name": "settings_unit", + "type": "INT", + "default": null, + "null": true, + "foreignTable": "unit", + "foreignKey": "unit_id" + }, "settings_app": { "name": "settings_app", "type": "INT", @@ -977,5 +1168,46 @@ "foreignKey": "account_id" } } + }, + "data_change": { + "name": "data_change", + "fields": { + "data_change_id": { + "name": "data_change_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "data_change_type": { + "name": "data_change_type", + "type": "VARCHAR(65)", + "null": false, + "unique": true + }, + "data_change_hash": { + "name": "data_change_hash", + "type": "VARCHAR(65)", + "null": false, + "unique": true + }, + "data_change_data": { + "name": "data_change_data", + "type": "VARCHAR(255)", + "null": false + }, + "data_change_created_by": { + "name": "data_change_created_by", + "type": "INT", + "null": false, + "foreignTable": "account", + "foreignKey": "account_id" + }, + "data_change_created_at": { + "name": "data_change_created_at", + "type": "DATETIME", + "null": false + } + } } } \ No newline at end of file diff --git a/Admin/Installer.php b/Admin/Installer.php index a5d85c6..91ec15d 100755 --- a/Admin/Installer.php +++ b/Admin/Installer.php @@ -78,7 +78,7 @@ final class Installer extends InstallerAbstract **/ private static function installDefaultSettings() : void { - SettingMapper::create()->execute(new Setting(0, SettingsEnum::PASSWORD_PATTERN, '', module: 'Admin')); + SettingMapper::create()->execute(new Setting(0, SettingsEnum::PASSWORD_PATTERN, '^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$', module: 'Admin')); SettingMapper::create()->execute(new Setting(0, SettingsEnum::LOGIN_TRIES, '3', '\\d+', module: 'Admin')); SettingMapper::create()->execute(new Setting(0, SettingsEnum::LOGIN_TIMEOUT, '3', '\\d+', module: 'Admin')); SettingMapper::create()->execute(new Setting(0, SettingsEnum::PASSWORD_INTERVAL, '90', '\\d+', module: 'Admin')); @@ -86,7 +86,7 @@ final class Installer extends InstallerAbstract SettingMapper::create()->execute(new Setting(0, SettingsEnum::LOGGING_STATUS, '1', '[0-3]', module: 'Admin')); SettingMapper::create()->execute(new Setting(0, SettingsEnum::LOGGING_PATH, '', module: 'Admin')); - SettingMapper::create()->execute(new Setting(0, SettingsEnum::DEFAULT_ORGANIZATION, '1', '\\d+', module: 'Admin')); + SettingMapper::create()->execute(new Setting(0, SettingsEnum::DEFAULT_UNIT, '1', '\\d+', module: 'Admin')); SettingMapper::create()->execute(new Setting(0, SettingsEnum::LOGIN_STATUS, '1', '[0-3]', module: 'Admin')); @@ -101,6 +101,8 @@ final class Installer extends InstallerAbstract SettingMapper::create()->execute(new Setting(0, SettingsEnum::MAIL_SERVER_KEYPASS, '', module: 'Admin')); SettingMapper::create()->execute(new Setting(0, SettingsEnum::MAIL_SERVER_TLS, (string) false, module: 'Admin')); + SettingMapper::create()->execute(new Setting(0, SettingsEnum::GROUP_GENERATE_AUTOMATICALLY_APP, (string) true, module: 'Admin')); + $cmdResult = \shell_exec( (OperatingSystem::getSystem() === SystemType::WIN ? 'php.exe' diff --git a/Controller/ApiController.php b/Controller/ApiController.php index 4d2591d..a7de22b 100755 --- a/Controller/ApiController.php +++ b/Controller/ApiController.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace Modules\Admin\Controller; +use Model\Setting; +use Model\SettingMapper; use Modules\Admin\Models\Account; use Modules\Admin\Models\AccountCredentialMapper; use Modules\Admin\Models\AccountMapper; @@ -36,6 +38,9 @@ use Modules\Media\Models\UploadFile; use Modules\Admin\Models\App; use phpOMS\Application\ApplicationType; use Modules\Admin\Models\AppMapper; +use Modules\Admin\Models\DataChange; +use Modules\Admin\Models\NullDataChange; +use Modules\Admin\Models\DataChangeMapper; use phpOMS\Account\AccountStatus; use phpOMS\Account\AccountType; use phpOMS\Account\GroupStatus; @@ -420,6 +425,7 @@ final class ApiController extends Controller 'response' => $this->app->appSettings->get( $id !== null ? (int) $id : $id, $request->getData('name') ?? '', + $request->getData('unit') ?? null, $request->getData('app') ?? null, $request->getData('module') ?? null, $group !== null ? (int) $group : $group, @@ -478,36 +484,133 @@ final class ApiController extends Controller $id = isset($data['id']) ? (int) $data['id'] : null; $name = $data['name'] ?? null; $content = $data['content'] ?? null; + $unit = $data['unit'] ?? null; $app = $data['app'] ?? null; $module = $data['module'] ?? null; $group = isset($data['group']) ? (int) $data['group'] : null; $account = isset($data['account']) ? (int) $data['account'] : null; - $this->updateModel( - $request->header->account, - $this->app->appSettings->get($id, $name, $app, $module, $group, $account), - $data, - function () use ($id, $name, $content, $app, $module, $group, $account) : void { - $this->app->appSettings->set([ - [ - 'id' => $id, - 'name' => $name, - 'content' => $content, - 'app' => $app, - 'module' => $module, - 'group' => $group, - 'account' => $account, - ], - ], true); - }, - 'settings', - $request->getOrigin() - ); + $old = $this->app->appSettings->get($id, $name, $unit, $app, $module, $group, $account); + $new = clone $old; + + $new->name = $name ?? $new->name; + $new->content = $content ?? $new->content; + $new->unit = $unit ?? $new->unit; + $new->app = $app ?? $new->app; + $new->module = $module ?? $new->module; + $new->group = $group ?? $new->group; + $new->account = $account ?? $new->account; + + $this->app->appSettings->set([ + [ + 'id' => $new->id, + 'name' => $new->name, + 'content' => $new->content, + 'unit' => $new->unit, + 'app' => $new->app, + 'module' => $new->module, + 'group' => $new->group, + 'account' => $new->account + ] + ], false); + + $this->updateModel($request->header->account, $old, $new, SettingMapper::class, 'settings',$request->getOrigin()); } $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Settings', 'Settings successfully modified', $dataSettings); } + /** + * Api method for modifying account password + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiSettingsAccountPasswordSet(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + // has required data + if (!empty($val = $this->validatePasswordUpdate($request))) { + $response->set('password_update', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $requestAccount = $request->header->account; + + // request account is valid + if ($requestAccount <= 0) { + $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []); + $response->header->status = RequestStatusCode::R_403; + + return; + } + + $account = AccountMapper::get() + ->where('id', $requestAccount) + ->execute(); + + // test old password is correct + if (AccountMapper::login($account->login, (string) $request->getData('oldpass')) !== $requestAccount) { + $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []); + $response->header->status = RequestStatusCode::R_403; + + return; + } + + // test password repetition + if (((string) $request->getData('newpass')) !== ((string) $request->getData('reppass'))) { + $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []); + $response->header->status = RequestStatusCode::R_403; + + return; + } + + // test password complexity + $complexity = $this->app->appSettings->get(names: [SettingsEnum::PASSWORD_PATTERN], module: 'Admin'); + if (\preg_match($complexity->content, (string) $request->getData('newpass')) !== 1) { + $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []); + $response->header->status = RequestStatusCode::R_403; + + return; + } + + $account->generatePassword((string) $request->getData('newpass')); + + AccountMapper::update()->execute($account); + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Password', 'Password successfully modified', $account); + } + + /** + * Validate password update request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validatePasswordUpdate(RequestAbstract $request) : array + { + $val = []; + if (($val['oldpass'] = empty($request->getData('oldpass'))) + || ($val['newpass'] = empty($request->getData('newpass'))) + || ($val['reppass'] = empty($request->getData('reppass'))) + ) { + return $val; + } + + return []; + } + /** * Api method for modifying account localization * @@ -529,7 +632,7 @@ final class ApiController extends Controller if ($requestAccount !== $accountId && !$this->app->accountManager->get($accountId)->hasPermission( PermissionType::MODIFY, - $this->app->orgId, + $this->app->unitId, $this->app->appName, self::NAME, PermissionCategory::ACCOUNT_SETTINGS, @@ -703,11 +806,33 @@ final class ApiController extends Controller } $app = $this->createApplicationFromRequest($request); - $this->createModel($request->header->account, $app, AppMapper::class, 'application', $request->getOrigin()); + + $this->createDefaultAppSettings($app, $request); + /** @var \Model\Setting $setting */ + $setting = $this->app->appSettings->get(null, SettingsEnum::GROUP_GENERATE_AUTOMATICALLY_APP); + if ($setting->content === '1') { + $newRequest = new HttpRequest(); + $newRequest->header->account = $request->header->account; + $newRequest->setData('name', 'app:' . \strtolower($app->name)); + $newRequest->setData('status', GroupStatus::ACTIVE); + $this->apiGroupCreate($newRequest, $response, $data); + } + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Application', 'Application successfully created', $app); } + private function createDefaultAppSettings(App $app, RequestAbstract $request) : void + { + $settings = []; + $settings[] = new Setting(0, SettingsEnum::REGISTRATION_ALLOWED, '0', '\\d+', app: $app->getId(), module: 'Admin'); + $settings[] = new Setting(0, SettingsEnum::APP_DEFAULT_GROUPS, '[]', app: $app->getId(), module: 'Admin'); + + foreach ($settings as $setting) { + $this->createModel($request->header->account, $setting, SettingMapper::class, 'setting', $request->getOrigin()); + } + } + /** * Validate app create request * @@ -1148,6 +1273,37 @@ final class ApiController extends Controller $this->createProfileForAccount($account, $request); $this->createMediaDirForAccount($account->getId(), $account->login ?? '', $request->header->account); + // find default groups and create them + $defaultGroups = []; + $defaultGroupIds = []; + + if ($request->hasData('app')) { + $defaultGroupSettings = $this->app->appSettings->get( + names: SettingsEnum::APP_DEFAULT_GROUPS, + app: (int) $request->getData('app'), + module: 'Admin' + ); + $defaultGroups = \array_merge($defaultGroups, \json_decode($defaultGroupSettings->content, true)); + } + + + if ($request->hasData('unit')) { + $defaultGroupSettings = $this->app->appSettings->get( + names: SettingsEnum::UNIT_DEFAULT_GROUPS, + unit: (int) $request->getData('unit'), + module: 'Admin' + ); + $defaultGroups = \array_merge($defaultGroups, \json_decode($defaultGroupSettings->content, true)); + } + + foreach ($defaultGroups as $group) { + $defaultGroupIds[] = $group->getId(); + } + + if (!empty($defaultGroupIds)) { + $this->createModelRelation($account->getId(), $account->getId(), $defaultGroupIds, AccountMapper::class, 'groups', 'account', $request->getOrigin()); + } + $this->fillJsonResponse( $request, $response, @@ -1160,6 +1316,240 @@ final class ApiController extends Controller ); } + public function apiAccountRegister(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + if (!empty($val = $this->validateRegistration($request))) { + $response->set('account_registration', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $allowed = $this->app->appSettings->get( + names: [SettingsEnum::REGISTRATION_ALLOWED], + app: (int) $request->getData('app'), + module: 'Admin' + ); + + if ($allowed->content !== '1') { + $this->fillJsonResponse($request, $response, NotificationLevel::ERROR, 'Registration', 'Registration not allowed', []); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $complexity = $this->app->appSettings->get(names: [SettingsEnum::PASSWORD_PATTERN], module: 'Admin'); + if ($request->hasData('password') + && \preg_match($complexity->content, (string) $request->getData('password')) !== 1 + ) { + $this->fillJsonResponse($request, $response, NotificationLevel::ERROR, 'Registration', 'Invalid password format', []); + $response->header->status = RequestStatusCode::R_403; + + return; + } + + // Check if account already exists + /** @var Account $emailAccount */ + $emailAccount = AccountMapper::get()->where('email', (string) $request->getData('email'))->execute(); + + /** @var Account $loginAccount */ + $loginAccount = AccountMapper::get()->where('login', (string) ($request->getData('login') ?? $request->getData('email')))->execute(); + + /** @var null|Account $account */ + $account = null; + + // email already in use + if (!($emailAccount instanceof NullAccount) + && AccountMapper::login($emailAccount->login, (string) $request->getData('password')) !== LoginReturnType::OK + ) { + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Registration', 'Email already in use, use your login details to login or activate your account also for this service.', []); + $response->header->status = RequestStatusCode::R_400; + + return; + } elseif (!($emailAccount instanceof NullAccount)) { + $account = $emailAccount; + } + + // login already in use by different email + if ($account === null + && !($loginAccount instanceof NullAccount) + && $loginAccount->getEmail() !== $request->getData('email') + ) { + $this->fillJsonResponse($request, $response, NotificationLevel::ERROR, 'Registration', 'Login already in use with a different email', []); + $response->header->status = RequestStatusCode::R_400; + + return; + } elseif ($account === null + && !($loginAccount instanceof NullAccount) + && AccountMapper::login($loginAccount->login, (string) $request->getData('password')) !== LoginReturnType::OK + ) { + $account = $loginAccount; + } + + $defaultGroups = []; + $defaultGroupIds = []; + + $defaultGroupSettings = $this->app->appSettings->get( + names: SettingsEnum::APP_DEFAULT_GROUPS, + app: (int) $request->getData('app'), + module: 'Admin' + ); + $defaultGroups = \array_merge($defaultGroups, \json_decode($defaultGroupSettings->content, true)); + + $defaultGroupSettings = $this->app->appSettings->get( + names: SettingsEnum::UNIT_DEFAULT_GROUPS, + unit: (int) $request->getData('unit'), + module: 'Admin' + ); + $defaultGroups = \array_merge($defaultGroups, \json_decode($defaultGroupSettings->content, true)); + + foreach ($defaultGroups as $group) { + $defaultGroupIds[] = $group->getId(); + } + + // Already registered + if ($account !== null) { + $account = AccountMapper::get() + ->with('groups') + ->where('id', $account->getId()) + ->execute(); + + foreach ($defaultGroupIds as $index => $id) { + if ($account->hasGroup($id)) { + unset($defaultGroupIds[$index]); + } + } + + if (empty($defaultGroupIds) + && $account->getStatus() === AccountStatus::ACTIVE + ) { + $this->fillJsonResponse($request, $response, NotificationLevel::ERROR, 'Registration', 'You are already registered, use your login data.', []); + $response->header->status = RequestStatusCode::R_403; + + return; + } elseif (empty($defaultGroupIds) + && $account->getStatus() === AccountStatus::INACTIVE + ) { + $this->fillJsonResponse($request, $response, NotificationLevel::ERROR, 'Registration', 'You are already registered, please activate your account through the email we sent you.', []); + $response->header->status = RequestStatusCode::R_403; + + return; + } + + // Create missing account / group relationships + $this->createModelRelation($account->getId(), $account->getId(), $defaultGroupIds, AccountMapper::class, 'groups', 'registration', $request->getOrigin()); + } else { + $request->setData('status', AccountStatus::INACTIVE); + $request->setData('type', AccountType::USER); + $request->setData('name1', !$request->hasData('name1') + ? \explode('@', $request->getData('email'))[0] + : $request->getData('name1') + ); + $request->setData('login', $request->getData('login') ?? $request->getData('email')); + + $this->apiAccountCreate($request, $response, $data); + $account = $response->get($request->uri->__toString())['response']; + + // Create confirmation pending entry + $dataChange = new DataChange(); + $dataChange->type = 'account'; + $dataChange->createdBy = $account->getId(); + + $dataChange->data = \json_encode([ + 'status' => AccountStatus::ACTIVE + ]); + + $tries = 0; + do { + $dataChange->reHash(); + $this->createModel($account->getId(), $dataChange, DataChangeMapper::class, 'datachange', $request->getOrigin()); + + ++$tries; + } while($dataChange->getId() === 0 && $tries < 5); + } + + // Create confirmation email + // @todo: send email for activation + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Registration', 'We have sent you an email to confirm your registration.', $account); + } + + /** + * Method to validate account registration from request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateRegistration(RequestAbstract $request) : array + { + $val = []; + if (($val['email'] = !empty($request->getData('email')) + && !EmailValidator::isValid((string) $request->getData('email'))) + || ($val['unit'] = empty($request->getData('unit'))) + || ($val['app'] = empty($request->getData('app'))) + || ($val['password'] = empty($request->getData('password'))) + ) { + return $val; + } + + return []; + } + + // @todo: maybe move to job/workflow??? This feels very much like a job/event especially if we make the 'type' an event-trigger + public function apiDataChange(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + if (!empty($val = $this->validateDataChange($request))) { + $response->set('data_change', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + /** @var DataChange $dataChange */ + $dataChange = DataChangeMapper::get()->where('hash', (string) $request->getData('hash'))->execute(); + if ($dataChange instanceof NullDataChange) { + $response->header->status = RequestStatusCode::R_400; + + return; + } + + switch ($dataChange->type) { + case 'account': + $old = AccountMapper::get()->where('id', $dataChange->createdBy)->execute(); + $new = clone $old; + + $data = \json_decode($dataChange->data, true); + $new->setStatus((int) $data['status']); + + $this->updateModel($dataChange->createdBy, $old, $new, AccountMapper::class, 'datachange', $request->getOrigin()); + $this->deleteModel($dataChange->createdBy, $dataChange, DataChangeMapper::class, 'datachange', $request->getOrigin()); + + break; + } + } + + /** + * Method to validate account registration from request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateDataChange(RequestAbstract $request) : array + { + $val = []; + if (($val['hash'] = empty($request->getData('hash')))) { + return $val; + } + + return []; + } + /** * Create directory for an account * @@ -1285,11 +1675,18 @@ final class ApiController extends Controller public function apiAccountUpdate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { /** @var Account $old */ - $old = AccountMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $old = AccountMapper::get() + ->where('id', (int) $request->getData('id')) + ->execute(); + $new = $this->updateAccountFromRequest($request, clone $old); $this->updateModel($request->header->account, $old, $new, AccountMapper::class, 'account', $request->getOrigin()); - if (\Modules\Profile\Models\ProfileMapper::get()->where('account', $new->getId())->execute() instanceof \Modules\Profile\Models\NullProfile) { + $profile = \Modules\Profile\Models\ProfileMapper::get() + ->where('account', $new->getId()) + ->execute(); + + if ($profile instanceof \Modules\Profile\Models\NullProfile) { $this->createProfileForAccount($new, $request); } @@ -1418,6 +1815,7 @@ final class ApiController extends Controller $moduleObj->theme = 'Default'; $moduleObj->path = $moduleInfo->getDirectory(); $moduleObj->version = $moduleInfo->getVersion(); + $moduleObj->name = $moduleInfo->getExternalName(); $moduleObj->setStatus(ModuleStatus::AVAILABLE); @@ -1725,7 +2123,7 @@ final class ApiController extends Controller $permission->setUnit(empty($request->getData('permissionunit')) ? null : (int) $request->getData('permissionunit')); $permission->setApp(empty($request->getData('permissionapp')) ? null : (string) $request->getData('permissionapp')); $permission->setModule(empty($request->getData('permissionmodule')) ? null : (string) $request->getData('permissionmodule')); - $permission->setCategory(empty($request->getData('permissiontype')) ? null : (int) $request->getData('permissiontype')); + $permission->setCategory(empty($request->getData('permissioncategory')) ? null : (int) $request->getData('permissioncategory')); $permission->setElement(empty($request->getData('permissionelement')) ? null : (int) $request->getData('permissionelement')); $permission->setComponent(empty($request->getData('permissioncomponent')) ? null : (int) $request->getData('permissioncomponent')); $permission->setPermission( @@ -1811,7 +2209,7 @@ final class ApiController extends Controller $permission->setUnit(empty($request->getData('permissionunit')) ? $permission->getUnit() : (int) $request->getData('permissionunit')); $permission->setApp(empty($request->getData('permissionapp')) ? $permission->getApp() : (string) $request->getData('permissionapp')); $permission->setModule(empty($request->getData('permissionmodule')) ? $permission->getModule() : (string) $request->getData('permissionmodule')); - $permission->setCategory(empty($request->getData('permissiontype')) ? $permission->getCategory() : (int) $request->getData('permissiontype')); + $permission->setCategory(empty($request->getData('permissioncategory')) ? $permission->getCategory() : (int) $request->getData('permissioncategory')); $permission->setElement(empty($request->getData('permissionelement')) ? $permission->getElement() : (int) $request->getData('permissionelement')); $permission->setComponent(empty($request->getData('permissioncomponent')) ? $permission->getComponent() : (int) $request->getData('permissioncomponent')); $permission->setPermission((int) ($request->getData('permissioncreate') ?? 0) diff --git a/Controller/BackendController.php b/Controller/BackendController.php index c25534f..42a8d03 100755 --- a/Controller/BackendController.php +++ b/Controller/BackendController.php @@ -720,7 +720,7 @@ final class BackendController extends Controller /** @var \Model\Setting[] $generalSettings */ $generalSettings = $this->app->appSettings->get( names: [ - SettingsEnum::PASSWORD_PATTERN, SettingsEnum::LOGIN_TIMEOUT, SettingsEnum::PASSWORD_INTERVAL, SettingsEnum::PASSWORD_HISTORY, SettingsEnum::LOGIN_TRIES, SettingsEnum::LOGGING_STATUS, SettingsEnum::LOGGING_PATH, SettingsEnum::DEFAULT_ORGANIZATION, + SettingsEnum::PASSWORD_PATTERN, SettingsEnum::LOGIN_TIMEOUT, SettingsEnum::PASSWORD_INTERVAL, SettingsEnum::PASSWORD_HISTORY, SettingsEnum::LOGIN_TRIES, SettingsEnum::LOGGING_STATUS, SettingsEnum::LOGGING_PATH, SettingsEnum::DEFAULT_UNIT, SettingsEnum::LOGIN_STATUS, SettingsEnum::DEFAULT_LOCALIZATION, SettingsEnum::MAIL_SERVER_ADDR, ], module: 'Admin' diff --git a/Models/AccountMapper.php b/Models/AccountMapper.php index 079b877..832a90a 100755 --- a/Models/AccountMapper.php +++ b/Models/AccountMapper.php @@ -213,7 +213,7 @@ class AccountMapper extends DataMapperFactory 'account_lactive' => new \DateTime('now'), 'account_tries' => 0, ]) - ->where('account_login', '=', $login) + ->where('account_id', '=', (int) $result['account_id']) ->execute(); return $result['account_id']; @@ -230,7 +230,7 @@ class AccountMapper extends DataMapperFactory 'account_lactive' => new \DateTime('now'), 'account_tries' => 0, ]) - ->where('account_login', '=', $login) + ->where('account_id', '=', (int) $result['account_id']) ->execute(); return $result['account_id']; @@ -240,7 +240,7 @@ class AccountMapper extends DataMapperFactory ->set([ 'account_tries' => $result['account_tries'] + 1, ]) - ->where('account_login', '=', $login) + ->where('account_id', '=', (int) $result['account_id']) ->execute(); return LoginReturnType::WRONG_PASSWORD; diff --git a/Models/ApiKey.php b/Models/ApiKey.php new file mode 100644 index 0000000..289de91 --- /dev/null +++ b/Models/ApiKey.php @@ -0,0 +1,79 @@ +key = \random_bytes(128); + $this->createdAt = new \DateTimeImmutable('now'); + } +} diff --git a/Models/ApiKeyMapper.php b/Models/ApiKeyMapper.php new file mode 100644 index 0000000..da89ea4 --- /dev/null +++ b/Models/ApiKeyMapper.php @@ -0,0 +1,121 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'account_api_id' => ['name' => 'account_api_id', 'type' => 'int', 'internal' => 'id'], + 'account_api_key' => ['name' => 'account_api_key', 'type' => 'string', 'internal' => 'key'], + 'account_api_status' => ['name' => 'account_api_status', 'type' => 'int', 'internal' => 'status'], + 'account_api_account' => ['name' => 'account_api_account', 'type' => 'int', 'internal' => 'account'], + 'account_api_created_at' => ['name' => 'account_api_created_at', 'type' => 'DateTime', 'internal' => 'createdAt'], + ]; + + /** + * Model to use by the mapper. + * + * @var string + * @since 1.0.0 + */ + public const MODEL = ApiKey::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'account_api'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD ='account_api_id'; + + /** + * Created at column + * + * @var string + * @since 1.0.0 + */ + public const CREATED_AT = 'account_api_created_at'; + + public static function authenticateApiKey(string $api) : int + { + if (empty($api)) { + return LoginReturnType::WRONG_PASSWORD; + } + + try { + $result = null; + + $query = new Builder(self::$db); + $result = $query->select('account.account_id', 'account.account_status') + ->from('account') + ->innerJoin('account_api')->on('account.account_id', '=', 'account_api.account_api_account') + ->where('account_api.account_api_key', '=', $api) + ->execute() + ?->fetchAll(); + + if ($result === null || !isset($result[0])) { + return LoginReturnType::WRONG_USERNAME; // wrong api key + } + + $result = $result[0]; + + if ($result['account_status'] !== AccountStatus::ACTIVE) { + return LoginReturnType::INACTIVE; + } + + if (empty($result['account_password'])) { + return LoginReturnType::EMPTY_PASSWORD; + } + + $query->update('account') + ->set([ + 'account_lactive' => new \DateTime('now'), + ]) + ->where('account_id', '=', (int) $result['account_id']) + ->execute(); + + return (int) $result['account_id']; + } catch (\Exception $e) { + return LoginReturnType::FAILURE; // @codeCoverageIgnore + } + } +} diff --git a/Models/App.php b/Models/App.php index ca094ba..094ee71 100755 --- a/Models/App.php +++ b/Models/App.php @@ -25,7 +25,7 @@ use phpOMS\Application\ApplicationType; * @link https://jingga.app * @since 1.0.0 */ -class App +class App implements \JsonSerializable { /** * Id @@ -78,4 +78,25 @@ class App { return $this->id; } + + /** + * {@inheritdoc} + */ + public function toArray() : array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'type' => $this->type, + 'status' => $this->status, + ]; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return $this->toArray(); + } } diff --git a/Models/DataChange.php b/Models/DataChange.php new file mode 100644 index 0000000..18b0700 --- /dev/null +++ b/Models/DataChange.php @@ -0,0 +1,109 @@ +createdAt = new \DateTimeImmutable('now'); + $this->reHash(); + } + + /** + * Get id + * + * @return int + * + * @since 1.0.0 + */ + public function getId() : int + { + return $this->id; + } + + public function reHash() : void + { + $this->hash = \random_bytes(64); + } + + /** + * Get hash + * + * @return string + * + * @since 1.0.0 + */ + public function getHash() : string + { + return $this->hash; + } + + /** + * {@inheritdoc} + */ + public function toArray() : array + { + return [ + 'id' => $this->id, + 'data' => $this->data, + ]; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return $this->toArray(); + } +} diff --git a/Models/DataChangeMapper.php b/Models/DataChangeMapper.php new file mode 100644 index 0000000..8b27e80 --- /dev/null +++ b/Models/DataChangeMapper.php @@ -0,0 +1,67 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'data_change_id' => ['name' => 'data_change_id', 'type' => 'int', 'internal' => 'id'], + 'data_change_type' => ['name' => 'data_change_type', 'type' => 'string', 'internal' => 'type'], + 'data_change_hash' => ['name' => 'data_change_hash', 'type' => 'string', 'internal' => 'hash'], + 'data_change_data' => ['name' => 'data_change_data', 'type' => 'string', 'internal' => 'data'], + 'data_change_created_by' => ['name' => 'data_change_created_by', 'type' => 'int', 'internal' => 'createdBy'], + 'data_change_created_at' => ['name' => 'data_change_created_at', 'type' => 'DateTimeImmutable', 'internal' => 'createdAt'], + ]; + + /** + * Model to use by the mapper. + * + * @var string + * @since 1.0.0 + */ + public const MODEL = DataChange::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'data_change'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD ='data_change_id'; +} diff --git a/Models/GroupPermission.php b/Models/GroupPermission.php index 898150d..b9f119d 100755 --- a/Models/GroupPermission.php +++ b/Models/GroupPermission.php @@ -41,8 +41,8 @@ class GroupPermission extends PermissionAbstract * Constructor. * * @param int $group Group id - * @param null|int $unit Unit Unit to check (null if all are acceptable) - * @param null|string $app App App to check (null if all are acceptable) + * @param null|int $unit Unit to check (null if all are acceptable) + * @param null|string $app App to check (null if all are acceptable) * @param null|string $module Module to check (null if all are acceptable) * @param null|string $from Module providing this permission * @param null|int $category Category (e.g. customer) (null if all are acceptable) diff --git a/Models/ModuleMapper.php b/Models/ModuleMapper.php index 9ff91fc..0591a2d 100755 --- a/Models/ModuleMapper.php +++ b/Models/ModuleMapper.php @@ -34,6 +34,7 @@ final class ModuleMapper extends DataMapperFactory */ public const COLUMNS = [ 'module_id' => ['name' => 'module_id', 'type' => 'string', 'internal' => 'id'], + 'module_name' => ['name' => 'module_name', 'type' => 'string', 'internal' => 'name'], 'module_path' => ['name' => 'module_path', 'type' => 'string', 'internal' => 'path'], 'module_theme' => ['name' => 'module_theme', 'type' => 'string', 'internal' => 'theme'], 'module_version' => ['name' => 'module_version', 'type' => 'string', 'internal' => 'version'], diff --git a/Models/NullDataChange.php b/Models/NullDataChange.php new file mode 100644 index 0000000..ef47264 --- /dev/null +++ b/Models/NullDataChange.php @@ -0,0 +1,47 @@ +id = $id; + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return ['id' => $this->id]; + } +} diff --git a/Models/SettingsEnum.php b/Models/SettingsEnum.php index c8cb8a1..121f453 100755 --- a/Models/SettingsEnum.php +++ b/Models/SettingsEnum.php @@ -42,7 +42,9 @@ abstract class SettingsEnum extends Enum public const LOGGING_PATH = '1000000007'; /* Organization settings */ - public const DEFAULT_ORGANIZATION = '1000000009'; + public const DEFAULT_UNIT = '1000000008'; + + public const UNIT_DEFAULT_GROUPS = '1000000009'; /* Login settings */ public const LOGIN_FORGOTTEN_COUNT = '1000000010'; @@ -77,13 +79,14 @@ abstract class SettingsEnum extends Enum public const CLI_ACTIVE = '1000000023'; /* Global default templates */ - public const DEFAULT_PDF_EXPORT_TEMPLATE = '1000000024'; + public const DEFAULT_LIST_EXPORTS = '1000000024'; - public const DEFAULT_CSV_EXPORT_TEMPLATE = '1000000025'; + public const DEFAULT_LETTERS = '1000000025'; - public const DEFAULT_EXCEL_EXPORT_TEMPLATE = '1000000026'; + /* App settings */ + public const REGISTRATION_ALLOWED = '1000000029'; - public const DEFAULT_WORD_EXPORT_TEMPLATE = '1000000027'; + public const GROUP_GENERATE_AUTOMATICALLY_APP = '1000000030'; - public const DEFAULT_EMAIL_EXPORT_TEMPLATE = '1000000028'; + public const APP_DEFAULT_GROUPS = '1000000031'; } diff --git a/tests/Controller/ApiControllerTest.php b/tests/Controller/ApiControllerTest.php index e22eddd..df05953 100755 --- a/tests/Controller/ApiControllerTest.php +++ b/tests/Controller/ApiControllerTest.php @@ -59,7 +59,7 @@ final class ApiControllerTest extends \PHPUnit\Framework\TestCase }; $this->app->dbPool = $GLOBALS['dbpool']; - $this->app->orgId = 1; + $this->app->unitId = 1; $this->app->accountManager = new AccountManager($GLOBALS['session']); $this->app->appSettings = new CoreSettings(); $this->app->moduleManager = new ModuleManager($this->app, __DIR__ . '/../../../../Modules/');