Back to overview

Creating a vCard Export script button

Scripting & Buttons Published on 31.10.2025

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.

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_id and record_id are 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:

  1. Confirm the Action: It first calls showDialog() to ask the user for confirmation.
  2. Fetch the Data: If the user confirms, it calls fetchAndProcessRecord(). This function makes a secure API call to api/v1.php?action=get_record to 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.
  3. Generate and Download: On a successful API response, it passes the received data to generateAndDownloadVCard().
  4. 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:VCARD and ending with END: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:
    1. A Blob (Binary Large Object) is created from our vCard string. We specify its type as text/vcard.
    2. URL.createObjectURL(blob) creates a temporary, local URL that points to the data in the Blob.
    3. A hidden <a> link element is created in the document.
    4. We set its href to our Blob URL and add the download attribute with a desired filename (e.g., "John_Doe.vcf").
    5. We programmatically click the link with link.click(), which triggers the browser's download dialog.
    6. Finally, the temporary link is removed from the document.

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.

  1. Navigate to Settings Custom Buttons.
  2. Click "New Button".
  3. 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.
  4. 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.