Vendor Templates exports them
* (export_vendor_templates) and imports them (import_vendor_templates); the
* Vendor Templates pack on Maintenance > Starter Content is loaded and
* brought up to date the same way.
*
* The file is JSON: {"format": "itflow-vendor-templates", "version": 1,
* "exported_at", "templates": [...]}. Each template carries what a vendor
* made from it starts with - its name, what they are, the support line,
* hours, SLA, support email, site, status page and portal, and its notes -
* and, when asked for, the account manager. Never an account number or PIN:
* those stay in the install they belong to, and one coming in keeps its own.
* Not who should have one either: a new one comes in required of no one, and
* one replaced keeps its own. A template replaced from a file changes the
* vendors made from it that still said what it did (vendorTemplateFollow()).
*/
const VENDOR_TEMPLATE_FILE_FORMAT = 'itflow-vendor-templates';
const VENDOR_TEMPLATE_FILE_VERSION = 1;
/** The fields that go in a file - key => longest it may be (0 for notes). Never the account number or PIN. */
const VENDOR_TEMPLATE_FILE_FIELDS = ['description' => 200, 'phone_country_code' => 10, 'phone' => 200, 'extension' => 200, 'hours' => 200, 'sla' => 200, 'email' => 200,
'website' => 200, 'status_page' => 200, 'portal' => 200, 'notes' => 0];
/** ...and the account manager's, when asked for. */
const VENDOR_TEMPLATE_FILE_MANAGER_FIELDS = ['contact_name' => 200, 'contact_phone_country_code' => 10, 'contact_phone' => 200, 'contact_email' => 200];
/*
* Templates as a file's contents (an array, for json_encode()) - the ones in
* $ids, or every current one when $ids is empty, the archived ones too with
* $archived, and their account manager with $manager.
*/
function vendorTemplatesExport(array $ids = [], $archived = false, $manager = false) {
global $mysqli;
$ids = array_values(array_filter(array_map('intval', $ids)));
$where = $ids ? 'vendor_template_id IN (' . implode(',', $ids) . ')' : ($archived ? '1 = 1' : 'vendor_template_archived_at IS NULL');
$fields = VENDOR_TEMPLATE_FILE_FIELDS + ($manager ? VENDOR_TEMPLATE_FILE_MANAGER_FIELDS : []);
$templates = [];
$sql = mysqli_query($mysqli, "SELECT * FROM vendor_templates WHERE $where ORDER BY vendor_template_name ASC, vendor_template_id ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$template = ['name' => (string) $row['vendor_template_name']];
foreach (array_keys($fields) as $field) {
$template[$field] = (string) $row["vendor_template_$field"];
}
$templates[] = $template;
}
return ['format' => VENDOR_TEMPLATE_FILE_FORMAT, 'version' => VENDOR_TEMPLATE_FILE_VERSION, 'exported_at' => date('c'), 'templates' => $templates];
}
/** A file name for an export - its one template's name, or the date. */
function vendorTemplatesExportFileName(array $export) {
$templates = $export['templates'] ?? [];
if (count($templates) === 1) {
$slug = trim(preg_replace('/[^a-z0-9]+/', '-', strtolower($templates[0]['name'])), '-');
return 'vendor-template-' . ($slug !== '' ? substr($slug, 0, 60) : 'export') . '.json';
}
return 'vendor-templates-' . date('Y-m-d') . '.json';
}
/*
* One field from a file, made safe as the template window would save it -
* text tidied (softwareTemplateFileText() and softwareTemplateFileNotes(),
* shared with the license templates file), phone numbers as digits, sites
* without their scheme or a trailing slash, an email that isn't one dropped.
*/
function vendorTemplateFileValue($field, $value, $max) {
if (!$max) {
return softwareTemplateFileNotes($value);
}
$value = softwareTemplateFileText($value, $max);
if (in_array($field, ['phone_country_code', 'phone', 'extension', 'contact_phone_country_code', 'contact_phone'], true)) {
return substr(preg_replace('/[^0-9]/', '', $value), 0, $max);
}
if (in_array($field, ['website', 'status_page', 'portal'], true)) {
return mb_substr(rtrim(preg_replace('#^https?://#i', '', $value), '/'), 0, $max);
}
if (in_array($field, ['email', 'contact_email'], true) && $value !== '' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
return '';
}
return $value;
}
/*
* A file's templates, read and checked - from what export wrote, or the
* starter pack. Each made safe (vendorTemplateFileValue()), with only the
* fields a file carries: an account number or PIN in it is left out. The
* account manager only when the file has one - 'manager' says whether it
* does. Null with $error saying why the file can't be used.
*/
function vendorTemplatesImportRead($contents, &$error = '') {
$data = is_array($contents) ? $contents : json_decode((string) $contents, true);
if (!is_array($data) || ($data['format'] ?? '') !== VENDOR_TEMPLATE_FILE_FORMAT || !isset($data['templates']) || !is_array($data['templates'])) {
$error = "That isn't a vendor templates file - export one from Admin > Vendor Templates.";
return null;
}
if (intval($data['version'] ?? 0) > VENDOR_TEMPLATE_FILE_VERSION) {
$error = 'That file was made by a newer version of ITFlow - update this one first.';
return null;
}
if (count($data['templates']) > 1000) {
$error = 'That file holds more than 1000 vendor templates - split it up.';
return null;
}
$templates = [];
foreach ($data['templates'] as $t) {
$name = is_array($t) ? softwareTemplateFileText($t['name'] ?? '', 200) : '';
if ($name === '') {
continue;
}
$template = ['name' => $name];
foreach (VENDOR_TEMPLATE_FILE_FIELDS as $field => $max) {
$template[$field] = vendorTemplateFileValue($field, $t[$field] ?? '', $max);
}
if ($template['phone'] === '') {
$template['phone_country_code'] = '';
}
$template['manager'] = false;
foreach (array_keys(VENDOR_TEMPLATE_FILE_MANAGER_FIELDS) as $field) {
$template['manager'] = $template['manager'] || array_key_exists($field, $t);
}
if ($template['manager']) {
foreach (VENDOR_TEMPLATE_FILE_MANAGER_FIELDS as $field => $max) {
$template[$field] = vendorTemplateFileValue($field, $t[$field] ?? '', $max);
}
}
$templates[] = $template;
}
if (!$templates) {
$error = 'There are no vendor templates in that file.';
return null;
}
return $templates;
}
/*
* Bring in templates read by vendorTemplatesImportRead(). 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') - every
* field the file carries, or only the ones in $only_fields (the starter
* pack's update). A template replaced keeps its own account number and PIN,
* its account manager unless the file has one, and who should have it; the
* vendors made from it that still said what it did change with it. A new one
* is required of no one. $only_existing: bring in only the ones already here
* (to update them). ['added', 'replaced', 'skipped', 'ids' of those added or
* replaced, 'followed' - [template name => vendorTemplateFollow()'s vendors]].
*/
function vendorTemplatesImport(array $templates, $existing = 'skip', $only_existing = false, array $only_fields = []) {
global $mysqli;
$counts = ['added' => 0, 'replaced' => 0, 'skipped' => 0, 'ids' => [], 'followed' => []];
$here = [];
$sql = mysqli_query($mysqli, "SELECT vendor_template_id, vendor_template_name FROM vendor_templates ORDER BY vendor_template_archived_at IS NOT NULL, vendor_template_id");
while ($row = mysqli_fetch_assoc($sql)) {
$key = mb_strtolower($row['vendor_template_name']);
$here[$key] = $here[$key] ?? intval($row['vendor_template_id']);
}
foreach ($templates as $t) {
$id = $here[mb_strtolower($t['name'])] ?? 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;
}
// What it has now - nothing, for a new one - with the file's fields put in
$before = $id ? vendorTemplateRowFields(vendorTemplateInfo($id)) : vendorTemplateRowFields([]);
$fields = $before;
$fields['name'] = $name;
$from_file = array_keys(VENDOR_TEMPLATE_FILE_FIELDS + (!empty($t['manager']) ? VENDOR_TEMPLATE_FILE_MANAGER_FIELDS : []));
foreach ($only_fields ? array_intersect($from_file, $only_fields) : $from_file as $field) {
$fields[$field] = (string) ($t[$field] ?? '');
}
if ($fields['phone'] === '') {
$fields['phone_country_code'] = '';
}
$saved = vendorTemplateSave($id, $fields);
$here[mb_strtolower($name)] = $saved;
$counts[$id ? 'replaced' : 'added']++;
$counts['ids'][] = $saved;
if ($id) {
$followed = vendorTemplateFollow($saved, $before, $fields);
if ($followed) {
$counts['followed'][$name] = $followed;
}
}
}
return $counts;
}
/* Each vendor that changed with a template brought in from a file, in its own history. */
function vendorTemplatesImportLogFollowed(array $counts) {
global $session_name;
foreach ($counts['followed'] as $template_name => $vendors) {
foreach ($vendors as $vendor_id => $vendor) {
logAudit("Vendor", "Edit", "$session_name updated vendor " . escapeSql($vendor['name']) . " from template " . escapeSql($template_name) . " - " . escapeSql(mb_strtolower(implode(', ', $vendor['fields']))), $vendor['client_id'], $vendor_id);
}
}
}
/* How many vendors changed with the templates brought in. */
function vendorTemplatesImportFollowedCount(array $counts) {
return array_sum(array_map('count', $counts['followed']));
}
/* What an import did, in words - for its message. HTML. */
function vendorTemplatesImportSummary(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;
}
}
$followed = vendorTemplatesImportFollowedCount($counts);
return ucfirst(implode(', ', $parts) ?: 'nothing brought in')
. ($followed ? '. ' . $followed . ' vendor' . ($followed === 1 ? '' : 's') . ' made from them changed with them' : '')
. '.';
}