Updated
This commit is contained in:
@@ -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_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 '<input type="hidden" name="assoc_profile_csrf" value="' . assoc_h($_SESSION['assoc_profile_csrf']) . '">';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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 '<div class="civicinfra-notice civicinfra-warning">Unknown management action.</div>';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// POST HANDLER
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_handle_post() {
|
||||
if (!assoc_verify_csrf()) {
|
||||
return '<div class="civicinfra-notice civicinfra-warning">Invalid form token.</div>';
|
||||
}
|
||||
|
||||
$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 '<div class="civicinfra-notice civicinfra-warning">Unknown action.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// SAVE ASSOCIATION
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_save_association() {
|
||||
$slug = substr(strip_tags($_POST['assoc_slug'] ?? ''), 0, 128);
|
||||
if (!$slug) return '<div class="alert alert-danger">Missing slug.</div>';
|
||||
|
||||
$registry = assoc_load_registry();
|
||||
if (!isset($registry[$slug])) {
|
||||
return '<div class="alert alert-danger">Association not found.</div>';
|
||||
}
|
||||
|
||||
$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 '<div class="alert alert-danger">Save failed. Check server logs.</div>';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// ADD ASSOCIATION
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_add_association() {
|
||||
$slug = strtolower(trim($_POST['new_slug'] ?? ''));
|
||||
$slug = preg_replace('/[^a-z0-9\-]/', '', $slug);
|
||||
if (!$slug) return '<div class="alert alert-danger">Invalid slug.</div>';
|
||||
|
||||
$registry = assoc_load_registry();
|
||||
if (isset($registry[$slug])) {
|
||||
return '<div class="alert alert-warning">Slug already exists: ' . assoc_h($slug) . '</div>';
|
||||
}
|
||||
|
||||
$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 '<div class="civicinfra-notice civicinfra-warning">Failed to create association.</div>';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// DELETE ASSOCIATION
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_delete_association() {
|
||||
$slug = substr(strip_tags($_POST['assoc_slug'] ?? ''), 0, 128);
|
||||
if (!$slug) return '<div class="civicinfra-notice civicinfra-warning">Missing slug.</div>';
|
||||
|
||||
$registry = assoc_load_registry();
|
||||
if (!isset($registry[$slug])) {
|
||||
return '<div class="civicinfra-notice civicinfra-warning">Association not found.</div>';
|
||||
}
|
||||
|
||||
$name = $registry[$slug]['assoc_legal_name'] ?? $slug;
|
||||
unset($registry[$slug]);
|
||||
|
||||
if (assoc_write_registry($registry)) {
|
||||
goaway(z_root() . '/assoc_profile/manage');
|
||||
}
|
||||
return '<div class="civicinfra-notice civicinfra-warning">Delete failed. Check server logs.</div>';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 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 '<div class="alert alert-danger">Save failed.</div>';
|
||||
}
|
||||
|
||||
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 '<div class="alert alert-danger">Nickname and label are required.</div>';
|
||||
}
|
||||
|
||||
$def = assoc_load_fields();
|
||||
foreach ($def['fields'] as $f) {
|
||||
if ($f['nickname'] === $nick) {
|
||||
return '<div class="alert alert-warning">Field already exists: ' . assoc_h($nick) . '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
$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 '<div class="alert alert-danger">Missing nickname.</div>';
|
||||
|
||||
$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'] .= '<div class="assoc-profile-edit-link mt-2">
|
||||
<a href="' . z_root() . '/assoc_profile/manage/assoc/' . assoc_h($slug) . '" class="btn btn-sm btn-outline-secondary">
|
||||
Edit Association Diagnostic Profile
|
||||
</a></div>';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 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 = '<div class="assoc-profile-view">';
|
||||
$out .= '<h4 class="assoc-profile-heading">Association Diagnostic Profile</h4>';
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$group_fields = $by_group[$group] ?? [];
|
||||
if (empty($group_fields)) continue;
|
||||
$out .= '<div class="assoc-profile-group">';
|
||||
$out .= '<h5 class="assoc-group-label">' . assoc_h($group) . '</h5>';
|
||||
foreach ($group_fields as $f) {
|
||||
$nick = $f['nickname'];
|
||||
$val = $entry[$nick] ?? '';
|
||||
if ($val === '') $val = '—';
|
||||
if ($val === 'UNK') $val = 'Unknown — not yet verified';
|
||||
$out .= '<div class="assoc-profile-field">';
|
||||
$out .= '<span class="assoc-field-label">' . assoc_h($f['label'] ?? $nick) . ':</span> ';
|
||||
$out .= '<span class="assoc-field-value">' . assoc_h($val) . '</span>';
|
||||
$out .= '</div>';
|
||||
}
|
||||
$out .= '</div>';
|
||||
}
|
||||
$out .= '</div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// CSRF
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_csrf_token() {
|
||||
if (empty($_SESSION['assoc_profile_csrf'])) {
|
||||
$_SESSION['assoc_profile_csrf'] = bin2hex(random_bytes(16));
|
||||
}
|
||||
return '<input type="hidden" name="assoc_profile_csrf" value="' . assoc_h($_SESSION['assoc_profile_csrf']) . '">';
|
||||
}
|
||||
|
||||
function assoc_verify_csrf() {
|
||||
return isset($_POST['assoc_profile_csrf'], $_SESSION['assoc_profile_csrf'])
|
||||
&& hash_equals($_SESSION['assoc_profile_csrf'], $_POST['assoc_profile_csrf']);
|
||||
}
|
||||
|
||||
121
hubzilla/addon/assoc_profile/assoc_profile_data.php
Normal file
121
hubzilla/addon/assoc_profile/assoc_profile_data.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Association Profile — Data Layer
|
||||
* File I/O and field helpers. No rendering, no POST handling.
|
||||
* Called via require_once in assoc_profile.php.
|
||||
*/
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// FILE I/O
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_load_config() {
|
||||
// Load config.json; return empty array on missing or malformed file.
|
||||
$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() {
|
||||
// Return path to assoc_registry.json, from config or default.
|
||||
$c = assoc_load_config();
|
||||
return $c['registry_file'] ?? 'addon/assoc_profile/assoc_registry.json';
|
||||
}
|
||||
|
||||
function assoc_fields_path() {
|
||||
// Return path to assoc_fields.json, from config or default.
|
||||
$c = assoc_load_config();
|
||||
return $c['fields_file'] ?? 'addon/assoc_profile/assoc_fields.json';
|
||||
}
|
||||
|
||||
function assoc_load_registry() {
|
||||
// Load and return the registry array, stripping meta keys.
|
||||
$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) {
|
||||
// Write the registry array to disk atomically; return true on success.
|
||||
$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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function assoc_load_fields() {
|
||||
// Load and return the fields definition array.
|
||||
$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) {
|
||||
// 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;
|
||||
}
|
||||
200
hubzilla/addon/assoc_profile/assoc_profile_post.php
Normal file
200
hubzilla/addon/assoc_profile/assoc_profile_post.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Association Profile — POST Handlers
|
||||
* Handles all form submissions from the /manage interface.
|
||||
* Called via require_once in assoc_profile.php.
|
||||
*/
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// POST DISPATCHER
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_handle_post() {
|
||||
// Verify CSRF then dispatch to the appropriate handler.
|
||||
if (!assoc_verify_csrf()) {
|
||||
return '<div class="civicinfra-notice civicinfra-warning">Invalid form token.</div>';
|
||||
}
|
||||
|
||||
$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 '<div class="civicinfra-notice civicinfra-warning">Unknown action.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 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 '<div class="civicinfra-notice civicinfra-warning">Missing slug.</div>';
|
||||
|
||||
$registry = assoc_load_registry();
|
||||
if (!isset($registry[$slug])) {
|
||||
return '<div class="civicinfra-notice civicinfra-warning">Association not found.</div>';
|
||||
}
|
||||
|
||||
$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 '<div class="civicinfra-notice civicinfra-warning">Save failed. Check server logs.</div>';
|
||||
}
|
||||
|
||||
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 '<div class="civicinfra-notice civicinfra-warning">Invalid slug.</div>';
|
||||
|
||||
$registry = assoc_load_registry();
|
||||
if (isset($registry[$slug])) {
|
||||
return '<div class="civicinfra-notice civicinfra-warning">Slug already exists: ' . assoc_h($slug) . '</div>';
|
||||
}
|
||||
|
||||
$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 '<div class="civicinfra-notice civicinfra-warning">Failed to create association.</div>';
|
||||
}
|
||||
|
||||
function assoc_delete_association() {
|
||||
// Permanently remove an association from the registry.
|
||||
$slug = substr(strip_tags($_POST['assoc_slug'] ?? ''), 0, 128);
|
||||
if (!$slug) return '<div class="civicinfra-notice civicinfra-warning">Missing slug.</div>';
|
||||
|
||||
$registry = assoc_load_registry();
|
||||
if (!isset($registry[$slug])) {
|
||||
return '<div class="civicinfra-notice civicinfra-warning">Association not found.</div>';
|
||||
}
|
||||
|
||||
unset($registry[$slug]);
|
||||
|
||||
if (assoc_write_registry($registry)) {
|
||||
goaway(z_root() . '/assoc_profile/manage');
|
||||
}
|
||||
return '<div class="civicinfra-notice civicinfra-warning">Delete failed. Check server logs.</div>';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 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 '<div class="civicinfra-notice civicinfra-warning">Save failed.</div>';
|
||||
}
|
||||
|
||||
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 '<div class="civicinfra-notice civicinfra-warning">Nickname and label are required.</div>';
|
||||
}
|
||||
|
||||
$def = assoc_load_fields();
|
||||
foreach ($def['fields'] as $f) {
|
||||
if ($f['nickname'] === $nick) {
|
||||
return '<div class="civicinfra-notice civicinfra-warning">Field already exists: ' . assoc_h($nick) . '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
$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 '<div class="civicinfra-notice civicinfra-warning">Missing nickname.</div>';
|
||||
|
||||
$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');
|
||||
}
|
||||
81
hubzilla/addon/assoc_profile/assoc_profile_view.php
Normal file
81
hubzilla/addon/assoc_profile/assoc_profile_view.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Association Profile — View Hooks and Profile Renderer
|
||||
* Renders the association profile on the public channel profile page.
|
||||
* Called via require_once in assoc_profile.php.
|
||||
*/
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// PROFILE VIEW HOOK — fires on profile_advanced
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_profile_view_hook(&$o) {
|
||||
// Append the association diagnostic profile to the channel profile page.
|
||||
$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) {
|
||||
// Add a link to the manage interface in the profile edit form.
|
||||
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'] .= '<div class="assoc-profile-edit-link mt-2">
|
||||
<a href="' . z_root() . '/assoc_profile/manage/assoc/' . assoc_h($slug) . '" class="btn btn-sm btn-outline-secondary">
|
||||
Edit Association Diagnostic Profile
|
||||
</a></div>';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 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 = '<div class="assoc-profile-view">';
|
||||
$out .= '<h4 class="assoc-profile-heading">Association Diagnostic Profile</h4>';
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$group_fields = $by_group[$group] ?? [];
|
||||
if (empty($group_fields)) continue;
|
||||
$out .= '<div class="assoc-profile-group">';
|
||||
$out .= '<h5 class="assoc-group-label">' . assoc_h($group) . '</h5>';
|
||||
foreach ($group_fields as $f) {
|
||||
$nick = $f['nickname'];
|
||||
$val = $entry[$nick] ?? '';
|
||||
if ($val === '') $val = '—';
|
||||
if ($val === 'UNK') $val = 'Unknown — not yet verified';
|
||||
$out .= '<div class="assoc-profile-field">';
|
||||
$out .= '<span class="assoc-field-label">' . assoc_h($f['label'] ?? $nick) . ':</span> ';
|
||||
$out .= '<span class="assoc-field-value">' . assoc_h($val) . '</span>';
|
||||
$out .= '</div>';
|
||||
}
|
||||
$out .= '</div>';
|
||||
}
|
||||
$out .= '</div>';
|
||||
return $out;
|
||||
}
|
||||
Reference in New Issue
Block a user