Creating a vCard Export script button
Creating a vCard Export Button
In our previous tutorials, we've seen how to display data and open external links. Now, let's build a button that generates a file on-the-fly and lets the user download it. We'll create a "Download vCard" button for a contact record, which will generate a standard .vcf file that can be imported into almost any address book application (like Outlook, Google Contacts, or your smartphone).
This tutorial introduces a powerful client-side pattern: fetching raw data from the server and then processing and formatting it directly in the user's browser with JavaScript to create a file for download.
What You'll Learn
This guide will teach you how to create a button script that:
- Securely fetches a single record's data using the application's API.
- Processes the data in JavaScript to build a string formatted to a specific standard (vCard).
- Handles special character escaping and line formatting required by the vCard specification.
- Generates a downloadable file (a
.vcfvCard) directly in the browser.
The Code: `vcard_export.php`
This file is a self-contained HTML document with embedded JavaScript. Its primary role is to orchestrate the data fetching and file generation process. Save this file as vcard_export.php inside a setup-specific directory like /setup_adressen/.
<?php
require_once '../init.php';
// --- Access Control & Initialisation ---
if (!isset($_SESSION['user'])) {
header('Location: ../login.php');
exit();
}
$table_id = (int)($_GET['table_id'] ?? 0);
$record_id = (int)($_GET['id'] ?? 0);
$sig_from_url = trim($_GET['sig'] ?? '');
?><!doctype html>
<html lang="<?= $current_lang ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>vCard Export...</title>
<link href="../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
<?php include '../theme.php'; ?>
</head>
<body>
<!-- This body is empty as the action runs via JavaScript modals -->
<script src="../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="../js/dialog.js"></script>
<script>
/**
* Escapes special characters for vCard values according to RFC 2426.
* @param {string} value - The string to escape.
* @returns {string} The escaped string.
*/
function vCardEscape(value) {
if (typeof value !== 'string') {
value = String(value);
}
value = value.replace(/\\/g, '\\\\'); // Escape backslashes
value = value.replace(/,/g, '\\,'); // Escape commas
value = value.replace(/;/g, '\\;'); // Escape semicolons
return value;
}
/**
* Generates and triggers the download of a vCard file.
* @param {object} record - The contact data object.
*/
function generateAndDownloadVCard(record) {
/**
* Folds a long line according to vCard specification (RFC 2426).
* Lines longer than 75 characters are split and continued on the next line
* with a leading space.
* @param {string} line - The line to fold.
* @returns {string} The folded line.
*/
function foldLine(line) {
const maxLen = 75;
if (line.length <= maxLen) {
return line;
}
let result = '';
let currentLine = line;
while (currentLine.length > maxLen) {
result += currentLine.substring(0, maxLen) + '\r\n ';
currentLine = currentLine.substring(maxLen);
}
result += currentLine;
return result;
}
let vcard = "BEGIN:VCARD\r\n";
vcard += "VERSION:3.0\r\n";
// Name (N) and Formatted Name (FN)
const vorname = record.vorname || '';
const nachname = record.nachname || '';
const titel = vCardEscape(record.titel || '');
const anrede = vCardEscape(record.anrede || '');
vcard += `N;CHARSET=UTF-8:${vCardEscape(nachname)};${vCardEscape(vorname)};;${titel};${anrede}\r\n`;
vcard += `FN;CHARSET=UTF-8:${vCardEscape(vorname)} ${vCardEscape(nachname)}\r\n`;
// Organization and Position
if (record.firma) {
vcard += foldLine(`ORG;CHARSET=UTF-8:${vCardEscape(record.firma || '')}${record.abteilung ? ';' + vCardEscape(record.abteilung) : ''}`) + '\r\n';
}
if (record.stellung) vcard += foldLine(`TITLE;CHARSET=UTF-8:${vCardEscape(record.stellung)}`) + '\r\n';
// Phone numbers
if (record.telefon) vcard += `TEL;TYPE=WORK,VOICE:${vCardEscape(record.telefon)}\r\n`;
if (record.telefon_privat) vcard += `TEL;TYPE=HOME,VOICE:${vCardEscape(record.telefon_privat)}\r\n`;
if (record.mobil) vcard += `TEL;TYPE=CELL:${vCardEscape(record.mobil)}\r\n`;
if (record.mobil_privat) vcard += `TEL;TYPE=CELL,HOME:${vCardEscape(record.mobil_privat)}\r\n`;
if (record.telefax) vcard += `TEL;TYPE=FAX:${vCardEscape(record.telefax)}\r\n`;
// Addresses (Work)
const strasse = vCardEscape(record.strasse || '');
const ort = vCardEscape(record.ort || '');
const bundesland = vCardEscape(record.bundesland || '');
const plz = vCardEscape(record.plz || '');
const staat = vCardEscape(record.staat || '');
if (strasse || ort) {
vcard += `ADR;TYPE=WORK;CHARSET=UTF-8:;;${strasse};${ort};${bundesland};${plz};${staat}\r\n`;
}
// Email addresses
if (record.mail) vcard += `EMAIL;TYPE=WORK:${record.mail}\r\n`;
if (record.mail_privat) vcard += `EMAIL;TYPE=HOME:${record.mail_privat}\r\n`;
// Website
if (record.website) vcard += foldLine(`URL:${vCardEscape(record.website)}`) + '\r\n';
// Birthday
if (record.geburtstag) vcard += `BDAY:${record.geburtstag}\r\n`;
// Note
if (record.bemerkungentext) {
const note = (record.bemerkungentext || '').replace(/(\r\n|\n|\r)/gm, "\\n");
const escapedNote = vCardEscape(note);
vcard += foldLine(`NOTE;CHARSET=UTF-8:${escapedNote}`) + '\r\n';
}
vcard += "END:VCARD\r\n";
// Generate filename
let filename_name = `${vorname} ${nachname}`.trim();
if (!filename_name && record.firma) filename_name = record.firma;
if (!filename_name) filename_name = 'kontakt';
const filename = filename_name.replace(/[^a-zA-Z0-9\-\._]/g, '_') + '.vcf';
// Create a Blob and trigger download
const blob = new Blob([vcard], { type: 'text/vcard;charset=utf-8;', endings: 'transparent' });
const link = document.createElement("a");
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
/**
* Fetches the current record from the table via REST API.
*/
async function fetchAndProcessRecord() {
const recordId = <?= $record_id ?>;
if (recordId === 0) {
showDialog({ title: 'Fehler', text: 'Diese Aktion kann nur aus einem Datensatz heraus ausgeführt werden.', okText: 'OK' });
return;
}
const recordApiUrl = `../api/v1.php?action=get_record&table_id=<?= $table_id ?>&id=${recordId}&sig=<?= $sig_from_url ?>`;
const csrfToken = '<?= generate_csrf_token() ?>';
try {
const response = await fetch(recordApiUrl, {
method: 'GET',
headers: { 'X-CSRF-Token': csrfToken }
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
const errorMessage = errorData?.error?.message || `API-Fehler: ${response.status} ${response.statusText}`;
throw new Error(errorMessage);
}
const record = await response.json();
if (record && !record.error) {
generateAndDownloadVCard(record);
} else {
showDialog({ title: 'Fehler', text: record.error?.message || 'Datensatz nicht gefunden.', okText: 'OK' });
}
} catch (error) {
console.error('Fehler beim Abrufen der Daten:', error);
showDialog({ title: 'Fehler', text: `Ein Fehler ist aufgetreten: ${error.message}`, okText: 'OK' });
} finally {
// The window is closed automatically after a short delay to avoid interrupting the download.
setTimeout(() => window.close(), 1000);
}
}
// --- Main execution ---
showDialog({
title: 'vCard Export',
text: 'Möchten Sie die Kontaktdaten dieses Datensatzes als vCard (.vcf) herunterladen?',
okText: 'Herunterladen',
cancelText: 'Abbrechen'
}).then(confirmed => {
if (confirmed) {
fetchAndProcessRecord();
} else {
window.close();
}
});
</script>
</body>
</html>
Breaking Down the Code
This script is a perfect example of a client-side action. The PHP part is minimal, only handling security. The real work happens in the user's browser using JavaScript.
Part 1: The PHP Security Guard
The PHP code at the top of the file is identical to our previous examples. It performs essential security checks:
- It ensures a user is logged in.
- It validates that a valid
table_idandrecord_idare present. - Most importantly, it validates the cryptographic signature (
sig) to prevent unauthorized access.
If all checks pass, it serves the HTML page containing our JavaScript logic.
Part 2: The JavaScript Orchestrator
The main script flow is straightforward:
- Confirm the Action: It first calls
showDialog()to ask the user for confirmation. - Fetch the Data: If the user confirms, it calls
fetchAndProcessRecord(). This function makes a secure API call toapi/v1.php?action=get_recordto get the complete, up-to-date data for the record. This is a robust approach because it works regardless of whether the button was clicked in a list view or an edit form. - Generate and Download: On a successful API response, it passes the received data to
generateAndDownloadVCard(). - Close the Window: Finally, it closes the (now empty) pop-up window.
Part 3: The vCard Generator (`generateAndDownloadVCard`)
This is the core of our tutorial. This function takes the raw data object and transforms it into a correctly formatted vCard string.
- vCard Structure: It builds the vCard string line by line, starting with
BEGIN:VCARDand ending withEND:VCARD. - Data Mapping: It maps fields from our record (like `vorname`, `firma`, `telefon`) to standard vCard properties (like `N`, `FN`, `ORG`, `TEL`).
- Character Escaping: The helper function
vCardEscape()is crucial. The vCard standard requires special characters like commas, semicolons, and backslashes to be escaped with a backslash. This function ensures the data is compliant. - Line Folding: The
foldLine()helper function implements another important part of the vCard standard. Lines in a vCard file must not exceed 75 bytes. This function automatically splits long lines and indents them correctly. - File Generation: This is the clever part. Instead of creating a file on the server, we use modern browser APIs:
- A
Blob(Binary Large Object) is created from our vCard string. We specify its type astext/vcard. URL.createObjectURL(blob)creates a temporary, local URL that points to the data in the Blob.- A hidden
<a>link element is created in the document. - We set its
hrefto our Blob URL and add thedownloadattribute with a desired filename (e.g., "John_Doe.vcf"). - We programmatically click the link with
link.click(), which triggers the browser's download dialog. - Finally, the temporary link is removed from the document.
- A
This client-side generation method is fast, secure, and reduces server load, making it an excellent pattern for custom export functionalities.
Integrating the Button
The integration process is straightforward and follows the same pattern as our previous buttons.
- Navigate to Settings Custom Buttons.
- Click "New Button".
- Fill out the form:
- Button Display Name:
Download vCard - URL / Script Path:
setup_adressen/vcard_export.php - Icon: A good choice is
fa-address-card. - Color:
btn-success. - Display Context: Select "List View (per row)" or "Record View", as this action is specific to a single record.
- Button Display Name:
- Save the button. It will now appear in the "Actions" column of your table or in the action bar of the record edit form.
You've now built a sophisticated export button that generates a file entirely in the browser. This same technique can be used to create CSV, iCalendar (.ics), or any other text-based file format directly from your application's data.