pdo->query('SELECT * FROM ' . $table)->fetchAll(); } $safeSettings = []; foreach ($this->settings->all() as $key => $value) { if (!str_starts_with($key, 'mailgun.api_key') && !str_starts_with($key, 'security.')) { $safeSettings[$key] = $value; } } $package = [ 'format' => 'neon-ledger/budget-planner', 'version' => 1, 'exported_at' => date(DATE_ATOM), 'settings' => $safeSettings, 'data' => $data, ]; $json = json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($json === false) { throw new RuntimeException('Planner package could not be encoded.'); } return $json; } public function importPackage(string $json, bool $replace): array { $package = json_decode($json, true); if (!is_array($package) || ($package['format'] ?? '') !== 'neon-ledger/budget-planner' || (int) ($package['version'] ?? 0) !== 1 || !is_array($package['data'] ?? null) ) { throw new RuntimeException('This is not a compatible Neon Ledger planner package.'); } $inserted = []; $this->pdo->beginTransaction(); try { if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql') { $this->pdo->exec('SET FOREIGN_KEY_CHECKS=0'); } else { $this->pdo->exec('PRAGMA defer_foreign_keys = ON'); } if ($replace) { foreach (array_reverse(self::TABLES) as $table) { $this->pdo->exec('DELETE FROM ' . $table); } } foreach (self::TABLES as $table) { $rows = $package['data'][$table] ?? []; $inserted[$table] = 0; if (!is_array($rows)) { continue; } foreach ($rows as $row) { if (!is_array($row) || $row === []) { continue; } $columns = array_keys($row); $columnSql = implode(', ', array_map( fn (string $column): string => $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? '`' . $column . '`' : '"' . $column . '"', $columns )); $placeholders = implode(', ', array_map(static fn (string $column): string => ':' . $column, $columns)); $verb = $replace ? ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? 'REPLACE' : 'INSERT OR REPLACE') : ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? 'INSERT IGNORE' : 'INSERT OR IGNORE'); $statement = $this->pdo->prepare(sprintf('%s INTO %s (%s) VALUES (%s)', $verb, $table, $columnSql, $placeholders)); $statement->execute($row); $inserted[$table] += $statement->rowCount(); } } foreach ((array) ($package['settings'] ?? []) as $key => $value) { if (is_string($key) && !str_starts_with($key, 'mailgun.api_key') && !str_starts_with($key, 'security.')) { $this->settings->set($key, $value); } } if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql') { $this->pdo->exec('SET FOREIGN_KEY_CHECKS=1'); } $this->pdo->commit(); } catch (\Throwable $exception) { if ($this->pdo->inTransaction()) { $this->pdo->rollBack(); } throw $exception; } return $inserted; } public function html(string $month): string { $summary = $this->budget->summary($month); $items = $this->budget->budgetItems($month); $transactions = $this->budget->transactions($month); $symbol = (string) $this->settings->get('app.currency_symbol', '$'); $escape = static fn (mixed $value): string => htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); $money = static fn (mixed $value): string => Support::money((float) $value, $symbol); $itemRows = ''; foreach ($items as $item) { $itemRows .= sprintf( '
| Item | Category | Target | Paid | Owing |
|---|
| Date | Description | Category | Amount |
|---|
Exported ' . $escape(date(DATE_RFC2822)) . '
'; } public function pdf(string $month): string { $summary = $this->budget->summary($month); $items = $this->budget->budgetItems($month); $symbol = (string) $this->settings->get('app.currency_symbol', '$'); $lines = [ (string) $this->settings->get('app.name', 'Neon Ledger') . ' - ' . date('F Y', strtotime($month . '-01')), '', 'Resources: ' . Support::money($summary['gross_resources'], $symbol), 'Expenses: ' . Support::money($summary['expenses'], $symbol), 'Dormant movement: ' . Support::money($summary['dead_movement'], $symbol), 'Remaining: ' . Support::money($summary['remaining'], $symbol), '', 'BUDGET PROGRESS', ]; foreach ($items as $item) { $lines[] = sprintf( '%s | target %s | paid %s | owing %s', $item['name'], Support::money($item['planned_amount'], $symbol), Support::money($item['paid_amount'], $symbol), Support::money($item['remaining_amount'], $symbol) ); } $lines[] = ''; $lines[] = 'Generated ' . date(DATE_RFC2822); return $this->simplePdf($lines); } private function simplePdf(array $lines): string { $content = "BT\n/F1 11 Tf\n50 760 Td\n14 TL\n"; foreach ($lines as $index => $line) { $line = iconv('UTF-8', 'Windows-1252//TRANSLIT', (string) $line) ?: (string) $line; $line = str_replace(['\\', '(', ')'], ['\\\\', '\\(', '\\)'], $line); $content .= ($index === 0 ? '' : "T*\n") . '(' . $line . ") Tj\n"; } $content .= "ET\n"; $objects = [ '<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>', '<< /Length ' . strlen($content) . " >>\nstream\n" . $content . 'endstream', '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>', ]; $pdf = "%PDF-1.4\n"; $offsets = [0]; foreach ($objects as $number => $object) { $offsets[] = strlen($pdf); $pdf .= ($number + 1) . " 0 obj\n" . $object . "\nendobj\n"; } $xref = strlen($pdf); $pdf .= "xref\n0 " . (count($objects) + 1) . "\n0000000000 65535 f \n"; for ($index = 1; $index <= count($objects); $index++) { $pdf .= sprintf("%010d 00000 n \n", $offsets[$index]); } $pdf .= 'trailer << /Size ' . (count($objects) + 1) . " /Root 1 0 R >>\nstartxref\n" . $xref . "\n%%EOF"; return $pdf; } }