License Templates exports them * (export_software_templates) and imports them (import_software_templates); * the MSP starter pack on Maintenance > Starter Content is loaded the same way. * * The file is JSON: {"format": "itflow-license-templates", "version": 1, * "exported_at", "templates": [...]}. Each template carries what a license * made from it starts with - its name, version, summary, type, license type, * notes, seats and renewal term - its vendor, as the vendor template it comes * from (its name, summary, phone, email, website, status page, portal, hours, * SLA and notes, never an account number or code), and the name of the product it is sold as. Not * who should have one: that is the call of the install it goes into - a new * one comes in required of no one, and one replaced keeps its own. Coming in, * a vendor template is matched by name, and made from the file when there is * none; a product only by name - prices are yours, so none is made. */ const SOFTWARE_TEMPLATE_FILE_FORMAT = 'itflow-license-templates'; const SOFTWARE_TEMPLATE_FILE_VERSION = 1; /** The vendor template fields that go in a file - its own account numbers and codes don't. */ const SOFTWARE_TEMPLATE_FILE_VENDOR_FIELDS = ['description' => 200, 'phone_country_code' => 10, 'phone' => 200, 'extension' => 200, 'email' => 200, 'website' => 200, 'status_page' => 200, 'portal' => 200, 'hours' => 200, 'sla' => 200, 'notes' => 0]; /* * Templates as a file's contents (an array, for json_encode()) - the ones in * $ids, or every current one when $ids is empty, and the archived ones too * with $archived. */ function softwareTemplatesExport(array $ids = [], $archived = false) { global $mysqli; $ids = array_values(array_filter(array_map('intval', $ids))); $where = $ids ? 'software_template_id IN (' . implode(',', $ids) . ')' : ($archived ? '1 = 1' : 'software_template_archived_at IS NULL'); $templates = []; $sql = mysqli_query($mysqli, "SELECT software_templates.*, vendor_templates.*, product_name FROM software_templates LEFT JOIN vendor_templates ON vendor_template_id = software_template_vendor_template_id AND software_template_vendor_template_id > 0 LEFT JOIN products ON product_id = software_template_product_id AND software_template_product_id > 0 WHERE $where ORDER BY software_template_name ASC, software_template_id ASC"); while ($row = mysqli_fetch_assoc($sql)) { $vendor = null; if ($row['vendor_template_name'] !== null) { $vendor = ['name' => (string) $row['vendor_template_name']]; foreach (array_keys(SOFTWARE_TEMPLATE_FILE_VENDOR_FIELDS) as $field) { $vendor[$field] = (string) $row["vendor_template_$field"]; } } $templates[] = [ 'name' => (string) $row['software_template_name'], 'version' => (string) $row['software_template_version'], 'description' => (string) $row['software_template_description'], 'type' => (string) $row['software_template_type'], 'license_type' => (string) $row['software_template_license_type'], 'notes' => (string) $row['software_template_notes'], 'seats' => $row['software_template_seats'] === null ? null : intval($row['software_template_seats']), 'term' => (string) $row['software_template_term'], 'vendor' => $vendor, 'product' => $row['product_name'] !== null ? (string) $row['product_name'] : null, ]; } return ['format' => SOFTWARE_TEMPLATE_FILE_FORMAT, 'version' => SOFTWARE_TEMPLATE_FILE_VERSION, 'exported_at' => date('c'), 'templates' => $templates]; } /** A file name for an export - its one template's name, or the date. */ function softwareTemplatesExportFileName(array $export) { $templates = $export['templates'] ?? []; if (count($templates) === 1) { $slug = trim(preg_replace('/[^a-z0-9]+/', '-', strtolower($templates[0]['name'])), '-'); return 'license-template-' . ($slug !== '' ? substr($slug, 0, 60) : 'export') . '.json'; } return 'license-templates-' . date('Y-m-d') . '.json'; } /* One line of text from a file, tidied - no markup, no runs of spaces, no longer than $max. */ function softwareTemplateFileText($value, $max) { return is_scalar($value) ? trim(mb_substr(preg_replace('/\s+/u', ' ', strip_tags((string) $value)), 0, $max)) : ''; } /* Notes from a file - no markup, their lines kept, no longer than $max. */ function softwareTemplateFileNotes($value, $max = 20000) { return is_scalar($value) ? trim(mb_substr(str_replace("\r\n", "\n", strip_tags((string) $value)), 0, $max)) : ''; } /* * A file's templates, read and checked - from what export wrote, or the * starter pack. Each made safe: text tidied, seats a whole number or none, a * renewal term that is one, its vendor with a name and only the fields a * vendor template has, its product a name. A template with no type is Other. * Null with $error saying why the file can't be used. */ function softwareTemplatesImportRead($contents, &$error = '') { $data = is_array($contents) ? $contents : json_decode((string) $contents, true); if (!is_array($data) || ($data['format'] ?? '') !== SOFTWARE_TEMPLATE_FILE_FORMAT || !isset($data['templates']) || !is_array($data['templates'])) { $error = "That isn't a license templates file - export one from Admin > License Templates."; return null; } if (intval($data['version'] ?? 0) > SOFTWARE_TEMPLATE_FILE_VERSION) { $error = 'That file was made by a newer version of ITFlow - update this one first.'; return null; } if (count($data['templates']) > 500) { $error = 'That file holds more than 500 license templates - split it up.'; return null; } $templates = []; foreach ($data['templates'] as $t) { $name = is_array($t) ? softwareTemplateFileText($t['name'] ?? '', 200) : ''; if ($name === '') { continue; } $vendor = null; $vendor_name = is_array($t['vendor'] ?? null) ? softwareTemplateFileText($t['vendor']['name'] ?? '', 200) : ''; if ($vendor_name !== '') { $vendor = ['name' => $vendor_name]; foreach (SOFTWARE_TEMPLATE_FILE_VENDOR_FIELDS as $field => $max) { $vendor[$field] = $max ? softwareTemplateFileText($t['vendor'][$field] ?? '', $max) : softwareTemplateFileNotes($t['vendor'][$field] ?? ''); } } $seats = $t['seats'] ?? null; $term = is_string($t['term'] ?? null) ? $t['term'] : ''; $type = softwareTemplateFileText($t['type'] ?? '', 200); $templates[] = [ 'name' => $name, 'version' => softwareTemplateFileText($t['version'] ?? '', 200), 'description' => softwareTemplateFileText($t['description'] ?? '', 500), 'type' => $type !== '' ? $type : 'Other', 'license_type' => softwareTemplateFileText($t['license_type'] ?? '', 200), 'notes' => softwareTemplateFileNotes($t['notes'] ?? ''), 'seats' => is_numeric($seats) ? max(0, min(1000000, intval($seats))) : null, 'term' => isset(SOFTWARE_RENEWAL_TERMS[$term]) ? $term : '', 'vendor' => $vendor, 'product' => softwareTemplateFileText($t['product'] ?? '', 200), ]; } if (!$templates) { $error = 'There are no license templates in that file.'; return null; } return $templates; } /* * The vendor template for a file's vendor - one here by its name (a current * one first), or one made from the file ($made says so). Its id. */ function softwareTemplateImportVendor(array $vendor, &$made = false) { global $mysqli; $made = false; $row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT vendor_template_id FROM vendor_templates WHERE LOWER(vendor_template_name) = LOWER('" . escapeSql($vendor['name']) . "') ORDER BY vendor_template_archived_at IS NOT NULL, vendor_template_id ASC LIMIT 1")); if ($row) { return intval($row[0]); } $set = ["vendor_template_name = '" . escapeSql($vendor['name']) . "'"]; foreach (array_keys(SOFTWARE_TEMPLATE_FILE_VENDOR_FIELDS) as $field) { $value = (string) ($vendor[$field] ?? ''); if (in_array($field, ['phone_country_code', 'phone', 'extension'], true)) { $value = preg_replace('/[^0-9]/', '', $value); } if (in_array($field, ['website', 'status_page', 'portal'], true)) { $value = rtrim(preg_replace('#^https?://#i', '', $value), '/'); } $set[] = "vendor_template_$field = '" . escapeSql($value) . "'"; } mysqli_query($mysqli, "INSERT INTO vendor_templates SET " . implode(', ', $set)); $made = true; return intval(mysqli_insert_id($mysqli)); } /* The product a file's template is sold as - a current one here by its name. Its id, or 0. */ function softwareTemplateImportProduct($name) { global $mysqli; if ((string) $name === '') { return 0; } $row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT product_id FROM products WHERE LOWER(product_name) = LOWER('" . escapeSql($name) . "') AND product_archived_at IS NULL ORDER BY product_id ASC LIMIT 1")); return intval($row[0] ?? 0); } /* * Bring in templates read by softwareTemplatesImportRead(). One whose name is * already here is skipped ($existing 'skip'), added under a name of its own * ('copy') or has what the file says put in its place ('replace') - its name, * version, summary, type, license type, notes, seats, renewal term and * vendor, and the product it is sold as when the file's is here (else it * keeps its own). Its licenses stay made from it, and who should have one * stays as it was; a new one is required of no one. $only_existing: bring in * only the ones already here (to update them). ['added', 'replaced', * 'skipped', 'vendors' made, 'products' - the names of the products not here, * 'ids' of those added or replaced]. */ function softwareTemplatesImport(array $templates, $existing = 'skip', $only_existing = false) { global $mysqli; $counts = ['added' => 0, 'replaced' => 0, 'skipped' => 0, 'vendors' => 0, 'products' => [], 'ids' => []]; $here = []; $sql = mysqli_query($mysqli, "SELECT software_template_id, software_template_name, software_template_product_id FROM software_templates ORDER BY software_template_id"); while ($row = mysqli_fetch_assoc($sql)) { $key = mb_strtolower($row['software_template_name']); $here[$key] = $here[$key] ?? ['id' => intval($row['software_template_id']), 'product_id' => intval($row['software_template_product_id'])]; } foreach ($templates as $t) { $id = $here[mb_strtolower($t['name'])]['id'] ?? 0; if (($only_existing && !$id) || ($id && $existing === 'skip')) { $counts['skipped']++; continue; } $name = $t['name']; if ($id && $existing === 'copy') { for ($n = 2; isset($here[mb_strtolower($name)]); $n++) { $name = mb_substr($t['name'], 0, 190) . " ($n)"; } $id = 0; } $replacing = $id > 0; $vendor_template_id = 0; if ($t['vendor']) { $vendor_template_id = softwareTemplateImportVendor($t['vendor'], $vendor_made); $counts['vendors'] += $vendor_made ? 1 : 0; } $product_id = softwareTemplateImportProduct($t['product']); if ($t['product'] !== '' && !$product_id) { $counts['products'][mb_strtolower($t['product'])] = $t['product']; } if (!$product_id && $replacing) { $product_id = $here[mb_strtolower($t['name'])]['product_id']; } $saved = softwareTemplateSave($id, [ 'name' => $name, 'version' => $t['version'], 'description' => $t['description'], 'type' => $t['type'], 'license_type' => $t['license_type'], 'notes' => $t['notes'], 'seats' => $t['seats'], 'term' => $t['term'], 'vendor_template_id' => $vendor_template_id, 'product_id' => $product_id, ]); $here[mb_strtolower($name)] = ['id' => $saved, 'product_id' => $product_id]; $counts[$replacing ? 'replaced' : 'added']++; $counts['ids'][] = $saved; } $counts['products'] = array_values($counts['products']); return $counts; } /* What an import did, in words - for its message. HTML. */ function softwareTemplatesImportSummary(array $counts) { $parts = []; foreach (['added' => 'added', 'replaced' => 'replaced', 'skipped' => 'already here, left as they were'] as $key => $word) { if ($counts[$key]) { $parts[] = '' . $counts[$key] . ' ' . $word; } } $products = $counts['products']; return ucfirst(implode(', ', $parts) ?: 'nothing brought in') . ($counts['vendors'] ? '. Added ' . $counts['vendors'] . ' vendor template' . ($counts['vendors'] === 1 ? '' : 's') . ' they come from' : '') . ($products ? '. Not sold as ' . escapeHtml(implode(', ', array_slice($products, 0, 5))) . (count($products) > 5 ? ' and ' . (count($products) - 5) . ' more' : '') . ' - ' . (count($products) === 1 ? 'that product isn\'t' : 'those products aren\'t') . ' here, so pick what each is sold as on its page' : '') . '.'; }