diff --git a/hubzilla/addon/assoc_profile/assoc_profile.php b/hubzilla/addon/assoc_profile/assoc_profile.php
index 33f20e4..b3ec655 100644
--- a/hubzilla/addon/assoc_profile/assoc_profile.php
+++ b/hubzilla/addon/assoc_profile/assoc_profile.php
@@ -3,13 +3,16 @@
/**
* Name: Association Profile
* Description: Structured diagnostic identity fields for HOA association channels.
- * Version: 0.2.0
+ * Version: 0.3.0
* MinVersion: 11.0
* MaxVersion: 12.0
*/
function assoc_profile_module() {}
+require_once 'addon/assoc_profile/assoc_profile_data.php';
+require_once 'addon/assoc_profile/assoc_profile_post.php';
+require_once 'addon/assoc_profile/assoc_profile_view.php';
require_once 'addon/assoc_profile/assoc_profile_manage.php';
function assoc_profile_load() {
@@ -25,6 +28,7 @@ function assoc_profile_unload() {
}
function assoc_profile_load_pdl(&$b) {
+ // Load PDL layout only when this module is active.
if (!is_array($b) || empty($b['module']) || $b['module'] !== 'assoc_profile') {
return;
}
@@ -35,123 +39,42 @@ function assoc_profile_load_pdl(&$b) {
}
// ----------------------------------------------------------------------------
-// HELPERS
+// HELPERS — used by all files in this addon
// ----------------------------------------------------------------------------
function assoc_h($v) {
+ // HTML-escape a value for safe output.
return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
}
function assoc_slug_from_address($addr) {
+ // Extract the local part of a channel@node address.
return explode('@', $addr)[0] ?? '';
}
function assoc_is_operator() {
+ // True only when the logged-in channel is the current page channel.
if (!local_channel()) return false;
$channel = App::get_channel();
return local_channel() === intval($channel['channel_id']);
}
// ----------------------------------------------------------------------------
-// FILE I/O
+// CSRF — used by manage and post handlers
// ----------------------------------------------------------------------------
-function assoc_load_config() {
- $raw = @file_get_contents('addon/assoc_profile/config.json');
- if (!$raw) return [];
- $d = json_decode($raw, true);
- return json_last_error() === JSON_ERROR_NONE ? $d : [];
-}
-
-function assoc_registry_path() {
- $c = assoc_load_config();
- return $c['registry_file'] ?? 'addon/assoc_profile/assoc_registry.json';
-}
-
-function assoc_fields_path() {
- $c = assoc_load_config();
- return $c['fields_file'] ?? 'addon/assoc_profile/assoc_fields.json';
-}
-
-function assoc_load_registry() {
- $raw = @file_get_contents(assoc_registry_path());
- if (!$raw) { logger('assoc_profile: registry not found'); return []; }
- $d = json_decode($raw, true);
- if (json_last_error() !== JSON_ERROR_NONE) { logger('assoc_profile: registry malformed'); return []; }
- foreach (['_note','_version','_slug_format','_select_values'] as $k) unset($d[$k]);
- return $d;
-}
-
-function assoc_write_registry($data) {
- $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
- if (!$json) { logger('assoc_profile: encode failed'); return false; }
- if (file_put_contents(assoc_registry_path(), $json, LOCK_EX) === false) {
- logger('assoc_profile: write failed'); return false;
+function assoc_csrf_token() {
+ // Generate or return the session CSRF token as a hidden input.
+ if (empty($_SESSION['assoc_profile_csrf'])) {
+ $_SESSION['assoc_profile_csrf'] = bin2hex(random_bytes(16));
}
- return true;
+ return ' ';
}
-function assoc_load_fields() {
- $raw = @file_get_contents(assoc_fields_path());
- if (!$raw) { logger('assoc_profile: fields file not found'); return ['groups'=>[],'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;
+function assoc_verify_csrf() {
+ // Return true if the POST CSRF token matches the session token.
+ return isset($_POST['assoc_profile_csrf'], $_SESSION['assoc_profile_csrf'])
+ && hash_equals($_SESSION['assoc_profile_csrf'], $_POST['assoc_profile_csrf']);
}
// ----------------------------------------------------------------------------
@@ -159,6 +82,7 @@ function assoc_remove_field_from_all($nickname) {
// ----------------------------------------------------------------------------
function assoc_profile_content() {
+ // Load assets and route /assoc_profile/manage/* requests.
if (function_exists('head_add_css')) {
head_add_css('/addon/assoc_profile/view/css/assoc_profile.css');
}
@@ -196,288 +120,3 @@ function assoc_profile_content() {
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']);
-}
diff --git a/hubzilla/addon/assoc_profile/assoc_profile_data.php b/hubzilla/addon/assoc_profile/assoc_profile_data.php
new file mode 100644
index 0000000..3c79c7f
--- /dev/null
+++ b/hubzilla/addon/assoc_profile/assoc_profile_data.php
@@ -0,0 +1,121 @@
+[],'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) {
+ // Write the fields definition array to disk; return true on success.
+ $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) {
+ // Return a single registry entry by slug, or null if not found.
+ $r = assoc_load_registry();
+ return $r[$slug] ?? null;
+}
+
+// ----------------------------------------------------------------------------
+// FIELD HELPERS
+// ----------------------------------------------------------------------------
+
+function assoc_field_map() {
+ // Return fields array keyed by nickname.
+ $def = assoc_load_fields();
+ $map = [];
+ foreach ($def['fields'] ?? [] as $f) {
+ $map[$f['nickname']] = $f;
+ }
+ return $map;
+}
+
+function assoc_blank_entry() {
+ // Return a new registry entry with all fields set to their defaults.
+ $map = assoc_field_map();
+ $entry = [];
+ foreach ($map as $nick => $f) {
+ $entry[$nick] = $f['default'] ?? '';
+ }
+ return $entry;
+}
+
+function assoc_backfill_all($new_field_def) {
+ // Add a new field with its default to every existing registry entry.
+ $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) {
+ // Remove a field key from every registry entry.
+ $registry = assoc_load_registry();
+ foreach ($registry as $slug => &$entry) {
+ unset($entry[$nickname]);
+ }
+ unset($entry);
+ return $registry;
+}
diff --git a/hubzilla/addon/assoc_profile/assoc_profile_post.php b/hubzilla/addon/assoc_profile/assoc_profile_post.php
new file mode 100644
index 0000000..fdfe815
--- /dev/null
+++ b/hubzilla/addon/assoc_profile/assoc_profile_post.php
@@ -0,0 +1,200 @@
+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.
';
+ }
+}
+
+// ----------------------------------------------------------------------------
+// ASSOCIATION HANDLERS
+// ----------------------------------------------------------------------------
+
+function assoc_save_association() {
+ // Save edited fields for an existing 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.
';
+}
+
+function assoc_add_association() {
+ // Create a new blank association entry with the given slug.
+ $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.
';
+}
+
+function assoc_delete_association() {
+ // Permanently remove an association from the registry.
+ $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.
';
+ }
+
+ unset($registry[$slug]);
+
+ if (assoc_write_registry($registry)) {
+ goaway(z_root() . '/assoc_profile/manage');
+ }
+ return 'Delete failed. Check server logs.
';
+}
+
+// ----------------------------------------------------------------------------
+// FIELD HANDLERS
+// ----------------------------------------------------------------------------
+
+function assoc_save_fields() {
+ // Save label, group, and help text changes; optionally reorder groups.
+ $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);
+
+ 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() {
+ // Add a new field definition and backfill all existing associations.
+ $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);
+
+ $registry = assoc_backfill_all($new_field);
+ assoc_write_registry($registry);
+
+ goaway(z_root() . '/assoc_profile/manage/fields');
+}
+
+function assoc_remove_field() {
+ // Remove a field from the definition and from all registry entries.
+ $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');
+}
diff --git a/hubzilla/addon/assoc_profile/assoc_profile_view.php b/hubzilla/addon/assoc_profile/assoc_profile_view.php
new file mode 100644
index 0000000..6d90475
--- /dev/null
+++ b/hubzilla/addon/assoc_profile/assoc_profile_view.php
@@ -0,0 +1,81 @@
+
+
+ Edit Association Diagnostic Profile
+ ';
+}
+
+// ----------------------------------------------------------------------------
+// PROFILE VIEW RENDERER
+// ----------------------------------------------------------------------------
+
+function assoc_render_view($entry) {
+ // Render all field groups as a read-only diagnostic profile block.
+ $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;
+}