$module) { if (!empty($module['hidden'])) continue; $groups[$module['group']][$key] = $module['title']; } return $groups; } /** * page_admin_access (a flat list of owner account names) and page_admin_roles * (account name => list of granted module keys) are two separate config * arrays, but the panel edits them as one table: an "Owner" checkbox plus one * checkbox per module. Owner wins if an account somehow ended up in both. */ function acp_permissions_current(): array { global $config; $accessStored = setting('config:page_admin_access', null); $access = $accessStored !== null ? (json_decode($accessStored, true) ?: array()) : (array)znote_config_path($config, 'page_admin_access', array()); $rolesStored = setting('config:page_admin_roles', null); $roles = $rolesStored !== null ? (json_decode($rolesStored, true) ?: array()) : (array)znote_config_path($config, 'page_admin_roles', array()); $rows = array(); foreach ($access as $name) { $name = trim((string)$name); if ($name === '') continue; $rows[$name] = array('account' => $name, 'owner' => true, 'modules' => array()); } foreach ($roles as $name => $assigned) { $name = trim((string)$name); if ($name === '' || isset($rows[$name])) continue; $rows[$name] = array('account' => $name, 'owner' => false, 'modules' => array_values((array)$assigned)); } return array_values($rows); } /** One account's editable block: name + Owner toggle + a module checklist grouped like the sidebar. */ function acp_render_permission_account(string $key, $rowIndex, array $row, array $moduleGroups): string { $prefix = 'set[' . $key . '][rows][' . $rowIndex . ']'; $html = '
'; $html .= '
'; $html .= ''; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; foreach ($moduleGroups as $groupLabel => $modules) { $html .= '

' . h($groupLabel) . '

'; foreach ($modules as $modKey => $modTitle) { $checked = in_array($modKey, $row['modules'], true) ? 'checked' : ''; $html .= ''; } $html .= '
'; } $html .= '
'; $html .= '
'; return $html; } /** Turn a section label into a stable, URL/hash-friendly tab id. */ function acp_settings_tab_slug(string $label): string { $slug = strtolower($label); $slug = preg_replace('/[^a-z0-9]+/', '-', $slug); $slug = trim((string)$slug, '-'); return $slug !== '' ? $slug : 'section'; } /** Cast a submitted value for storage. */ function acp_setting_cast(array $field, $raw): string { switch ($field['type']) { case 'bool': return empty($raw) ? '0' : '1'; case 'int': $value = intv($raw); if (isset($field['min'])) $value = max((int)$field['min'], $value); if (isset($field['max'])) $value = min((int)$field['max'], $value); return (string)$value; case 'select': $options = $field['options'] ?? array(); $value = trim((string)$raw); return isset($options[$value]) ? $value : (string)array_key_first($options); case 'json': $decoded = json_decode(is_string($raw) ? $raw : '', true); return is_array($decoded) ? (string)json_encode($decoded, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : ''; case 'checklist': $options = $field['options'] ?? array(); $picked = is_array($raw) ? $raw : array(); $kept = array(); foreach (array_keys($options) as $option) { if (in_array((string)$option, array_map('strval', $picked), true)) { $kept[] = (string)$option; } } if (!$kept && $options) { $kept[] = (string)array_key_first($options); } if (!empty($field['int_values'])) { $kept = array_values(array_map('intval', $kept)); } return (string)json_encode($kept); default: return trim((string)$raw); } } /** * acp_table_rows_from_json() / acp_table_cell_to_string() / acp_table_cell_input() / * acp_table_cell_from_post() are defined in admin/bootstrap.php (shared with the * serverdata single-record editors). acp_table_json_from_rows() stays here because * this map-shape branch is specific to this module's numeric-id schema fields * (towns/vocations/skills) - it rejects non-digit keys, which a generic reuse * elsewhere should not inherit. */ /** Convert posted table rows back into the JSON structure used for storage. */ function acp_table_json_from_rows(array $field, array $rowsRaw): array { $shape = $field['json_shape'] ?? 'map'; if ($shape === 'list') { $json = array(); foreach ($rowsRaw as $row) { if (!is_array($row)) continue; $obj = array(); foreach ($field['columns'] as $colKey => $colDef) { $obj[$colKey] = acp_table_cell_from_post($colDef, $row[$colKey] ?? null); } // Skip fully blank rows (an "add row" the admin never filled in). $hasContent = false; foreach ($obj as $v) { if ($v !== '' && $v !== false) { $hasContent = true; break; } } if (!$hasContent) continue; $json[] = $obj; } return $json; } $rowKey = $field['row_key'] ?? 'id'; $valueColumn = $field['value_column'] ?? null; $json = array(); foreach ($rowsRaw as $row) { if (!is_array($row)) continue; $keyRaw = trim((string)($row[$rowKey] ?? '')); if ($keyRaw === '' || !ctype_digit($keyRaw)) continue; $key = (string)(int)$keyRaw; if ($valueColumn !== null) { $json[$key] = trim((string)($row[$valueColumn] ?? '')); continue; } $obj = array(); foreach ($field['columns'] as $colKey => $colDef) { if ($colKey === $rowKey) continue; $obj[$colKey] = acp_table_cell_from_post($colDef, $row[$colKey] ?? null); } $json[$key] = $obj; } return $json; } if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['clear_cache'])) { $removed = znote_cache_flush(); acp_log('cache.flush', '', ['entries' => $removed]); acp_flash_success(t_default('acp.settings.cache_cleared', 'Cache cleared: {n} entries removed.', ['n' => $removed])); acp_redirect('settings'); } // --------------------------------------------------------------------------- // Save // --------------------------------------------------------------------------- if ($_SERVER['REQUEST_METHOD'] === 'POST') { $schema = acp_settings_schema(); $saved = 0; $failed = 0; $savedKeys = array(); foreach ($schema as $fields) { foreach ($fields as $key => $field) { $raw = $_POST['set'][$key] ?? ''; if (($field['type'] ?? '') === 'permissions') { // Whoever is submitting this form, only a literal Owner may change // who has access. Without this check, an account merely granted // the 'settings' module could open this same tab and grant itself // Owner - the module-level check alone cannot catch that, since // this whole page is one form and 'settings' access already lets // the request reach this branch. if (!acp_is_owner()) { $failed++; acp_flash_error(t_default('acp.perm.owner_only', 'Only an Owner can change permissions.')); continue; } $rowsRaw = (is_array($raw) && isset($raw['rows']) && is_array($raw['rows'])) ? $raw['rows'] : array(); $validModules = array_keys(acp_modules()); $access = array(); $roles = array(); foreach ($rowsRaw as $row) { if (!is_array($row)) continue; $account = trim((string)($row['account'] ?? '')); if ($account === '') continue; if (!empty($row['owner'])) { $access[] = $account; continue; } $picked = array_values(array_intersect($validModules, array_map('strval', (array)($row['modules'] ?? array())))); if ($picked) { $roles[$account] = $picked; } } $access = array_values(array_unique($access)); // Never save a state with zero owners - that would lock every // admin, including the one submitting this form, out of the // panel with no way back in short of editing the database by hand. if (!$access) { $failed++; acp_flash_error(t_default('acp.perm.no_owner', 'At least one account must stay an Owner. Permissions were not saved.')); continue; } $accessOk = setting_set('config:page_admin_access', (string)json_encode($access)); $rolesOk = setting_set('config:page_admin_roles', (string)json_encode($roles, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); if ($accessOk && $rolesOk) { $saved++; $savedKeys[] = 'page_admin_access'; $savedKeys[] = 'page_admin_roles'; } else { $failed++; } continue; } if (($field['type'] ?? '') === 'table') { $rowsRaw = (is_array($raw) && isset($raw['rows']) && is_array($raw['rows'])) ? $raw['rows'] : array(); $jsonArr = acp_table_json_from_rows($field, $rowsRaw); if (setting_set('config:' . $key, (string)json_encode($jsonArr, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))) { $saved++; $savedKeys[] = $key; } else $failed++; continue; } if (($field['type'] ?? '') === 'json') { $decoded = json_decode(is_string($raw) ? $raw : '', true); if (!is_array($decoded)) { $failed++; continue; } if (setting_set('config:' . $key, (string)json_encode($decoded, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))) { $saved++; $savedKeys[] = $key; } else $failed++; continue; } $value = acp_setting_cast($field, $raw); if (setting_set('config:' . $key, $value)) { $saved++; $savedKeys[] = $key; } else $failed++; } } if ($saved > 0) { acp_log('settings.save', '', ['fields' => $savedKeys, 'failed' => $failed]); } if ($failed > 0) { acp_flash_error(t('acp.settings.save_failed', ['n' => $failed, 'table' => 'znote_config'])); } else { acp_flash_success(t('acp.settings.save_success', ['n' => $saved])); } acp_redirect('settings'); } $schema = acp_settings_schema(); $hasTable = znote_table_exists('znote_config'); $cacheStats = znote_cache_stats(); $cacheSize = $cacheStats['bytes'] >= 1048576 ? number_format($cacheStats['bytes'] / 1048576, 1) . ' MB' : number_format($cacheStats['bytes'] / 1024, 1) . ' KB'; ?>
'znote_config', 'file' => 'SQL/migrations/2.0.0_znote_config.sql', ]) ?>
'config.php', 'configphp2' => 'config.php', ]) ?>

ยท
APCu
$fields): $acpTabSlug = acp_settings_tab_slug((string)$section); ?>
$fields): $acpTabSlug = acp_settings_tab_slug((string)$section); ?>
>

$field): $stored = setting('config:' . $key, null); $fromFile = znote_config_path($config, $key, $field['default'] ?? ''); if (is_bool($fromFile)) { $fromFile = $fromFile ? '1' : '0'; } elseif (is_array($fromFile)) { if ($field['type'] === 'checklist') { $fromFile = (string)json_encode(array_values($fromFile)); } elseif ($field['type'] === 'json' || $field['type'] === 'table') { $fromFile = (string)json_encode($fromFile, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } else { $fromFile = ''; } } $current = ($stored !== null) ? $stored : (string)$fromFile; if (($field['type'] === 'json' || $field['type'] === 'table') && $stored !== null) { $decoded = json_decode($stored, true); if (is_array($decoded)) { $current = (string)json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } } $fromDb = ($stored !== null); ?>

$row): ?>
$row): ?> $colDef): ?>
$caption): ?>
min="" max="" value="">

'' . t('acp.settings.file_pill') . '']) ?>