[],'fields'=>[]]; } $d = json_decode($raw, true); if (json_last_error() !== JSON_ERROR_NONE) { logger('assoc_profile: fields file malformed'); return ['groups'=>[],'fields'=>[]]; } return $d; } function assoc_write_fields($data) { $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); if (!$json) return false; return file_put_contents(assoc_fields_path(), $json, LOCK_EX) !== false; } function assoc_get($slug) { $r = assoc_load_registry(); return $r[$slug] ?? null; } // ---------------------------------------------------------------------------- // FIELD HELPERS // ---------------------------------------------------------------------------- function assoc_field_map() { $def = assoc_load_fields(); $map = []; foreach ($def['fields'] ?? [] as $f) { $map[$f['nickname']] = $f; } return $map; } function assoc_blank_entry() { $map = assoc_field_map(); $entry = []; foreach ($map as $nick => $f) { $entry[$nick] = $f['default'] ?? ''; } return $entry; } function assoc_backfill_all($new_field_def) { $registry = assoc_load_registry(); $nick = $new_field_def['nickname']; $default = $new_field_def['default'] ?? ''; foreach ($registry as $slug => &$entry) { if (!isset($entry[$nick])) { $entry[$nick] = $default; } } unset($entry); return $registry; } function assoc_remove_field_from_all($nickname) { $registry = assoc_load_registry(); foreach ($registry as $slug => &$entry) { unset($entry[$nickname]); } unset($entry); return $registry; } // ---------------------------------------------------------------------------- // CONTENT ROUTER // ---------------------------------------------------------------------------- function assoc_profile_content() { if (function_exists('head_add_css')) { head_add_css('/addon/assoc_profile/view/css/assoc_profile.css'); } if (function_exists('head_add_js')) { head_add_js('/addon/assoc_profile/view/js/assoc_profile.js'); } $action = argv(1) ?? ''; if ($action !== 'manage') { return ''; } if (!assoc_is_operator()) { return '
Operator access required.
'; } $sub = argv(2) ?? ''; $slug = argv(3) ?? ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { return assoc_handle_post(); } if (!$sub) return assoc_render_manage_index(); if ($sub === 'assoc') { if (!$slug) return assoc_render_add_association_form(); return assoc_render_edit_association_form($slug); } if ($sub === 'fields') return assoc_render_fields_form(); if ($sub === 'import') return assoc_render_import_form(); return '
Unknown management action.
'; } // ---------------------------------------------------------------------------- // POST HANDLER // ---------------------------------------------------------------------------- function assoc_handle_post() { if (!assoc_verify_csrf()) { return '
Invalid form token.
'; } $action = $_POST['assoc_action'] ?? ''; switch ($action) { case 'save_association': return assoc_save_association(); case 'add_association': return assoc_add_association(); case 'delete_association':return assoc_delete_association(); case 'save_fields': return assoc_save_fields(); case 'add_field': return assoc_add_field(); case 'remove_field': return assoc_remove_field(); case 'export_selected': return assoc_handle_export($_POST['export_slugs'] ?? ''); case 'import_upload': return assoc_handle_import_upload(); case 'import_confirm': return assoc_handle_import_confirm(); default: return '
Unknown action.
'; } } // ---------------------------------------------------------------------------- // SAVE ASSOCIATION // ---------------------------------------------------------------------------- function assoc_save_association() { $slug = substr(strip_tags($_POST['assoc_slug'] ?? ''), 0, 128); if (!$slug) return '
Missing slug.
'; $registry = assoc_load_registry(); if (!isset($registry[$slug])) { return '
Association not found.
'; } $field_map = assoc_field_map(); foreach ($field_map as $nick => $fdef) { if (($fdef['type'] ?? '') === 'readonly') continue; if (!isset($_POST[$nick])) continue; $val = substr(strip_tags((string)$_POST[$nick]), 0, 1024); if (isset($fdef['options']) && !in_array($val, $fdef['options'], true)) continue; $registry[$slug][$nick] = $val; } $registry[$slug]['assoc_updated'] = date('Y-m-d'); if (assoc_write_registry($registry)) { goaway(z_root() . '/assoc_profile/manage/assoc/' . $slug); } return '
Save failed. Check server logs.
'; } // ---------------------------------------------------------------------------- // ADD ASSOCIATION // ---------------------------------------------------------------------------- function assoc_add_association() { $slug = strtolower(trim($_POST['new_slug'] ?? '')); $slug = preg_replace('/[^a-z0-9\-]/', '', $slug); if (!$slug) return '
Invalid slug.
'; $registry = assoc_load_registry(); if (isset($registry[$slug])) { return '
Slug already exists: ' . assoc_h($slug) . '
'; } $entry = assoc_blank_entry(); $entry['assoc_registered'] = date('Y-m-d'); $entry['assoc_updated'] = date('Y-m-d'); $registry[$slug] = $entry; if (assoc_write_registry($registry)) { goaway(z_root() . '/assoc_profile/manage/assoc/' . $slug); } return '
Failed to create association.
'; } // ---------------------------------------------------------------------------- // DELETE ASSOCIATION // ---------------------------------------------------------------------------- function assoc_delete_association() { $slug = substr(strip_tags($_POST['assoc_slug'] ?? ''), 0, 128); if (!$slug) return '
Missing slug.
'; $registry = assoc_load_registry(); if (!isset($registry[$slug])) { return '
Association not found.
'; } $name = $registry[$slug]['assoc_legal_name'] ?? $slug; unset($registry[$slug]); if (assoc_write_registry($registry)) { goaway(z_root() . '/assoc_profile/manage'); } return '
Delete failed. Check server logs.
'; } // ---------------------------------------------------------------------------- // FIELD MANAGEMENT // ---------------------------------------------------------------------------- function assoc_save_fields() { // Save label and group changes to existing fields $def = assoc_load_fields(); $fields = &$def['fields']; foreach ($fields as &$f) { $nick = $f['nickname']; if (isset($_POST['label_' . $nick])) { $f['label'] = substr(strip_tags($_POST['label_' . $nick]), 0, 128); } if (isset($_POST['group_' . $nick])) { $f['group'] = substr(strip_tags($_POST['group_' . $nick]), 0, 64); } if (isset($_POST['help_' . $nick])) { $f['help'] = substr(strip_tags($_POST['help_' . $nick]), 0, 256); } } unset($f); // Save group order if (!empty($_POST['groups'])) { $groups = array_map(function($g) { return substr(strip_tags($g), 0, 64); }, explode("\n", $_POST['groups'])); $def['groups'] = array_values(array_filter(array_map('trim', $groups))); } if (assoc_write_fields($def)) { goaway(z_root() . '/assoc_profile/manage/fields'); } return '
Save failed.
'; } function assoc_add_field() { $nick = strtolower(preg_replace('/[^a-z0-9_]/', '', $_POST['new_nickname'] ?? '')); $label = substr(strip_tags($_POST['new_label'] ?? ''), 0, 128); $type = in_array($_POST['new_type'] ?? '', ['text','select','readonly']) ? $_POST['new_type'] : 'text'; $group = substr(strip_tags($_POST['new_group'] ?? ''), 0, 64); $default = substr(strip_tags($_POST['new_default'] ?? ''), 0, 128); $help = substr(strip_tags($_POST['new_help'] ?? ''), 0, 256); if (!$nick || !$label) { return '
Nickname and label are required.
'; } $def = assoc_load_fields(); foreach ($def['fields'] as $f) { if ($f['nickname'] === $nick) { return '
Field already exists: ' . assoc_h($nick) . '
'; } } $new_field = [ 'nickname' => $nick, 'label' => $label, 'type' => $type, 'group' => $group, 'default' => $default, 'required' => false, 'help' => $help, ]; if ($type === 'select' && !empty($_POST['new_options'])) { $opts = array_map('trim', explode("\n", $_POST['new_options'])); $new_field['options'] = array_values(array_filter($opts)); } $def['fields'][] = $new_field; assoc_write_fields($def); // Backfill all existing associations $registry = assoc_backfill_all($new_field); assoc_write_registry($registry); goaway(z_root() . '/assoc_profile/manage/fields'); } function assoc_remove_field() { $nick = preg_replace('/[^a-z0-9_]/', '', $_POST['remove_nickname'] ?? ''); if (!$nick) return '
Missing nickname.
'; $def = assoc_load_fields(); $def['fields'] = array_values(array_filter($def['fields'], function($f) use ($nick) { return $f['nickname'] !== $nick; })); assoc_write_fields($def); $registry = assoc_remove_field_from_all($nick); assoc_write_registry($registry); goaway(z_root() . '/assoc_profile/manage/fields'); } // ---------------------------------------------------------------------------- // PROFILE VIEW HOOK // ---------------------------------------------------------------------------- function assoc_profile_view_hook(&$o) { $uid = intval(App::$profile['profile_uid'] ?? 0); if (!$uid) return; $r = q("SELECT channel_address FROM channel WHERE channel_id = %d LIMIT 1", intval($uid)); if (!$r) return; $slug = assoc_slug_from_address($r[0]['channel_address']); $entry = assoc_get($slug); if (!$entry) return; if (function_exists('head_add_css')) head_add_css('/addon/assoc_profile/view/css/assoc_profile.css'); $o .= assoc_render_view($entry); } // ---------------------------------------------------------------------------- // PROFILE EDIT HOOK // ---------------------------------------------------------------------------- function assoc_profile_edit_hook(&$b) { if (!assoc_is_operator()) return; $channel = App::get_channel(); $slug = assoc_slug_from_address($channel['channel_address'] ?? ''); $entry = assoc_get($slug); if (!$entry) return; if (function_exists('head_add_css')) head_add_css('/addon/assoc_profile/view/css/assoc_profile.css'); $b['html'] .= ''; } // ---------------------------------------------------------------------------- // RENDER — PROFILE VIEW // ---------------------------------------------------------------------------- function assoc_render_view($entry) { $def = assoc_load_fields(); $groups = $def['groups'] ?? []; $fields = $def['fields'] ?? []; $by_group = []; foreach ($fields as $f) { $by_group[$f['group']][] = $f; } $out = '
'; $out .= '

Association Diagnostic Profile

'; foreach ($groups as $group) { $group_fields = $by_group[$group] ?? []; if (empty($group_fields)) continue; $out .= '
'; $out .= '
' . assoc_h($group) . '
'; foreach ($group_fields as $f) { $nick = $f['nickname']; $val = $entry[$nick] ?? ''; if ($val === '') $val = '—'; if ($val === 'UNK') $val = 'Unknown — not yet verified'; $out .= '
'; $out .= '' . assoc_h($f['label'] ?? $nick) . ': '; $out .= '' . assoc_h($val) . ''; $out .= '
'; } $out .= '
'; } $out .= '
'; return $out; } // ---------------------------------------------------------------------------- // CSRF // ---------------------------------------------------------------------------- function assoc_csrf_token() { if (empty($_SESSION['assoc_profile_csrf'])) { $_SESSION['assoc_profile_csrf'] = bin2hex(random_bytes(16)); } return ''; } function assoc_verify_csrf() { return isset($_POST['assoc_profile_csrf'], $_SESSION['assoc_profile_csrf']) && hash_equals($_SESSION['assoc_profile_csrf'], $_POST['assoc_profile_csrf']); }