[],'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']);
}