Files
Nic Boet 656a94c9bd feat: make options set in zm.conf or conf.d read-only on the Options page refs #5289
zm.conf and conf.d are set by the system owner and win over the Config
table in PHP, Perl and, since 4d7609d36, the daemons. The Options page
still offered those options for editing and showed the database value,
with a note only when the two differed.

An option a file sets is now shown with the value in effect (a boolean
word read as in configFileValue()), its input disabled, and one line
naming the file ('OptionSetInConfigFile', through translate()). That
line replaces the old "overriden via configuration files" note. A row
whose option a file sets gets no data-requires, so the page script
doesn't enable its input again, and the Requires check of other options
reads the value in effect.

actions/options.php skips options a file sets. A disabled checkbox is
left out of the form, and the action saved a missing boolean as 0, so
without the skip a save of the tab would have reset the Config row; a
hand-made POST can't change it either. The Config row keeps whatever the
admin last saved, which applies again once the file line is removed.

loadConfigFiles() is split out of loadConfig() so the view and the
action can tell which names come from a file and which file. Each entry
carries that file; loadConfig() drops the key before returning, so
configs/viewByName answers as before.

docs/userguide/configfiles.rst describes setting an option in a file.
2026-10-16 09:24:48 -05:00

332 lines
13 KiB
PHP

<?php
//
// ZoneMinder web action file
// Copyright (C) 2019 ZoneMinder LLC
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// System edit actions
if ( !canEdit('System') ) {
ZM\Warning('Must have System permissions to perform options actions');
return;
}
global $error_message;
// AI object-detection CRUD lives in dedicated action files. The list views post
// here (view=options) with object=ai_* for deletes; route those to the matching
// handler, which processes both save and delete via $action.
if ( isset($_REQUEST['object']) && in_array($_REQUEST['object'], array('ai_model', 'ai_dataset', 'ai_class'), true) ) {
require_once('includes/actions/'.$_REQUEST['object'].'.php');
return;
}
if ( $action == 'delete' ) {
if ( isset($_REQUEST['object']) ) {
if ( $_REQUEST['object'] == 'server' ) {
if ( !empty($_REQUEST['markIds']) ) {
foreach ( $_REQUEST['markIds'] as $Id ) {
dbQuery('DELETE FROM Servers WHERE Id=?', array($Id));
ZM\AuditAction('delete', 'server', $Id, '');
}
}
$refreshParent = true;
} else if ( $_REQUEST['object'] == 'storage' ) {
if ( !empty($_REQUEST['markIds']) ) {
foreach ( $_REQUEST['markIds'] as $Id ) {
dbQuery('DELETE FROM Storage WHERE Id=?', array($Id));
ZM\AuditAction('delete', 'storage', $Id, '');
}
}
$refreshParent = true;
} else if ( $_REQUEST['object'] == 'role' ) {
if ( !empty($_REQUEST['markRids']) ) {
foreach ( $_REQUEST['markRids'] as $Id ) {
dbQuery('DELETE FROM User_Roles WHERE Id=?', array($Id));
ZM\AuditAction('delete', 'role', $Id, '');
}
}
$redirect = '?view=options&tab=roles';
} # end if isset($_REQUEST['object'] )
} else if ( isset($_REQUEST['markUids']) ) {
// deletes users
foreach ($_REQUEST['markUids'] as $markUid) {
dbQuery('DELETE FROM Users WHERE Id = ?', array($markUid));
ZM\AuditAction('delete', 'user', $markUid, '');
}
if ($markUid == $user->Id()) {
userLogout();
$redirect = '?view=login';
} else {
$redirect = '?view=options&tab=users';
}
}
} else if ( $action == 'options' && isset($_REQUEST['tab']) ) {
$result = dbQuery('SELECT Name,Value,Type,`System` FROM Config WHERE Category=? ORDER BY Id ASC', array($_REQUEST['tab']));
if (!$result) {
$error_message .= 'Error loading config for tab ' . htmlspecialchars($_REQUEST['tab']) . ': ' . htmlspecialchars(dbLastError()) . '<br/>';
return;
}
// Options set in zm.conf or conf.d belong to the system owner and can't be changed here: skip
// a submitted value, and a boolean left out of the form (a disabled checkbox isn't sent),
// which would otherwise be saved as 0.
$fileConfig = loadConfigFiles();
$changed = false;
while ($config = dbFetchNext($result)) {
unset($newValue);
if (isset($fileConfig[$config['Name']])) {
if (isset($_REQUEST['newConfig'][$config['Name']]))
ZM\Debug('Not saving '.$config['Name'].': it is set in '.$fileConfig[$config['Name']]['File']);
continue;
}
if ( ($config['Type'] == 'boolean') and empty($_REQUEST['newConfig'][$config['Name']]) ) {
$newValue = 0;
} else if (isset($_REQUEST['newConfig'][$config['Name']])) {
$newValue = preg_replace("/\r\n/", "\n", $_REQUEST['newConfig'][$config['Name']]);
}
if (isset($newValue) && ($newValue != $config['Value'])) {
# Handle special cases first
if ($config['Name'] == 'ZM_LANG_DEFAULT') {
# Verify that the language file exists in the lang directory.
if (!file_exists(ZM_PATH_WEB.'/lang/'.$newValue.'.php')) {
$error_message .= 'Error setting ' . $config['Name'].'. New value ' .$newValue.' not saved because '.ZM_PATH_WEB.'/lang/'.$newValue.'.php doesn\'t exist.<br/>';
ZM\Error($error_message);
continue;
}
}
if (dbQuery('UPDATE Config SET Value=? WHERE Name=?', array($newValue, $config['Name'])) === null) {
$error_message .= 'Error saving ' . htmlspecialchars($config['Name']) . ': ' . htmlspecialchars(dbLastError()) . '<br/>';
ZM\Error('Error saving config '.$config['Name'].': '.dbLastError());
} else {
$changed = true;
}
} # end if value changed
} # end foreach config entry
if ( $changed ) {
ZM\AuditAction('update', 'config', 0, 'Tab: '.$_REQUEST['tab']);
switch ( $_REQUEST['tab'] ) {
case 'system' :
case 'config' :
$restartWarning = true;
break;
case 'API':
case 'web' :
case 'tools' :
break;
case 'logging' :
case 'network' :
case 'mail' :
case 'upload' :
$restartWarning = true;
break;
case 'highband' :
case 'medband' :
case 'lowband' :
break;
}
loadConfig(false);
# Might need to update auth hash
# This doesn't work because the config are constants and won't really be loaded until the next refresh.
#generateAuthHash(ZM_AUTH_HASH_IPS, true);
}
# On error stay on the page (no redirect) so the error is shown; otherwise
# redirect to drop the POST and reload with fresh values.
if (!$error_message) {
$redirect = '?view=options&tab='.$_REQUEST['tab'];
}
return;
} else if ($action == 'save') {
if (isset($_REQUEST['object'])) {
if ($_REQUEST['object'] == 'dnsmasq') {
$config = isset($_REQUEST['config']) ? $_REQUEST['config'] : [];
$conf = '';
foreach ($config as $name=>$value) {
if ($name == 'dhcp-host') {
foreach ($value as $mac=>$ip) {
$conf .= $name.'='.$mac.','.$ip.PHP_EOL;
}
} else if ($name == 'dhcp-option') {
foreach ($value as $option_name=>$option_value) {
$conf .= $name.'='.$option_name.','.$option_value.PHP_EOL;
}
} else if (
($name == 'bind-interfaces')
or
($name == 'dhcp-authoritative')
) {
if ($value=='yes') {
$conf .= $name.PHP_EOL;
}
} else if ($name == 'dhcp-range') {
$conf .= $name.'='.$value['min'].','.$value['max'].','.$value['expires'].PHP_EOL;
} else {
if (is_array($value)) {
foreach ($value as $v) {
}
} else {
$conf .= $name.'='.$value.PHP_EOL;
}
}
}
if (false===file_put_contents(ZM_PATH_DNSMASQ_CONF, $conf)) {
ZM\Warning("Failed to writh to ".ZM_PATH_DNSMASQ_CONF);
} else {
exec('sudo -n /bin/systemctl restart dnsmasq.service');
}
exec('sudo -n /bin/systemctl restart dnsmasq.service');
}
}
} else if ($action == 'start') {
if (isset($_REQUEST['object'])) {
if ($_REQUEST['object'] == 'dnsmasq') {
exec('sudo -n /bin/systemctl start dnsmasq.service', $output, $result);
if ($result) {
ZM\Warning("Error execing sudo -n /bin/systemctl start dnsmasq.service. Output: ".implode(PHP_EOL, $output));
} else {
ZM\Debug("Error execing sudo -n /bin/systemctl start dnsmasq.service. Output: ".implode(PHP_EOL, $output));
}
}
}
} else if ($action == 'stop') {
if (isset($_REQUEST['object'])) {
if ($_REQUEST['object'] == 'dnsmasq') {
exec('sudo -n /bin/systemctl stop dnsmasq.service', $output, $result);
if ($result) {
ZM\Warning("Error execing sudo -n /bin/systemctl start dnsmasq.service. Output: ".implode(PHP_EOL, $output));
} else {
ZM\Debug("Error execing sudo -n /bin/systemctl start dnsmasq.service. Output: ".implode(PHP_EOL, $output));
}
}
}
} else if ($action == 'menuitems') {
if (!canEdit('System')) {
ZM\Warning('Need System permission to edit menu items');
} else if (isset($_REQUEST['items'])) {
require_once('includes/MenuItem.php');
$allItems = ZM\MenuItem::find();
foreach ($allItems as $item) {
$id = $item->Id();
$enabled = isset($_REQUEST['items'][$id]['Enabled']) ? 1 : 0;
$label = isset($_REQUEST['items'][$id]['Label']) ? trim($_REQUEST['items'][$id]['Label']) : null;
$sortOrder = isset($_REQUEST['items'][$id]['SortOrder']) ? intval($_REQUEST['items'][$id]['SortOrder']) : $item->SortOrder();
if ($label === '') $label = null;
$iconType = isset($_REQUEST['items'][$id]['IconType']) ? $_REQUEST['items'][$id]['IconType'] : $item->IconType();
if (!in_array($iconType, ['material', 'fontawesome', 'image', 'none'])) $iconType = 'material';
$icon = isset($_REQUEST['items'][$id]['Icon']) ? trim($_REQUEST['items'][$id]['Icon']) : $item->Icon();
if ($icon === '') $icon = null;
$link = isset($_REQUEST['items'][$id]['Link']) ? trim($_REQUEST['items'][$id]['Link']) : $item->Link();
if ($link === '') $link = null;
// The MenuKey is only editable for custom (non-built-in) entries, so it
// is only present in the request for those rows; keep it otherwise.
$menuKey = isset($_REQUEST['items'][$id]['MenuKey']) ? trim($_REQUEST['items'][$id]['MenuKey']) : $item->MenuKey();
if ($menuKey === '') $menuKey = $item->MenuKey();
if (!$item->save([
'MenuKey' => $menuKey,
'Enabled' => $enabled,
'Label' => $label,
'SortOrder' => $sortOrder,
'Icon' => $icon,
'IconType' => $iconType,
'Link' => $link,
])) {
$error_message .= 'Failed to save menu item ' . htmlspecialchars($menuKey) . ': ' . htmlspecialchars($item->get_last_error()) . '<br/>';
}
}
// Insert any new menu entries added via the "Add" button.
if (isset($_REQUEST['newItems']) && is_array($_REQUEST['newItems'])) {
foreach ($_REQUEST['newItems'] as $new) {
$menuKey = isset($new['MenuKey']) ? trim($new['MenuKey']) : '';
if ($menuKey === '') continue;
$enabled = isset($new['Enabled']) ? 1 : 0;
$label = isset($new['Label']) ? trim($new['Label']) : null;
if ($label === '') $label = null;
$sortOrder = isset($new['SortOrder']) ? intval($new['SortOrder']) : 0;
$iconType = isset($new['IconType']) ? $new['IconType'] : 'material';
if (!in_array($iconType, ['material', 'fontawesome', 'image', 'none'])) $iconType = 'material';
$icon = isset($new['Icon']) ? trim($new['Icon']) : null;
if ($icon === '') $icon = null;
$link = isset($new['Link']) ? trim($new['Link']) : null;
if ($link === '') $link = null;
$newItem = new ZM\MenuItem();
if (!$newItem->save([
'MenuKey' => $menuKey,
'Enabled' => $enabled,
'Label' => $label,
'SortOrder' => $sortOrder,
'Icon' => $icon,
'IconType' => $iconType,
'Link' => $link,
])) {
$error_message .= 'Failed to add menu item ' . htmlspecialchars($menuKey) . ': ' . htmlspecialchars($newItem->get_last_error()) . '<br/>';
}
}
}
}
# Stay on the page when something failed so the error is shown.
if (!$error_message) {
$redirect = '?view=options&tab=menu';
}
} else if ($action == 'deletemenuitems') {
if (!canEdit('System')) {
ZM\Warning('Need System permission to delete menu items');
} else if (!empty($_REQUEST['deleteIds']) && is_array($_REQUEST['deleteIds'])) {
$ids = array_values(array_filter(array_map('intval', $_REQUEST['deleteIds']), function($v) { return $v > 0; }));
if (count($ids)) {
$placeholders = implode(',', array_fill(0, count($ids), '?'));
if (dbQuery('DELETE FROM `Menu_Items` WHERE `Id` IN ('.$placeholders.')', $ids) === null) {
$error_message .= 'Failed to delete menu entries: ' . htmlspecialchars(dbLastError()) . '<br/>';
}
}
}
if (!$error_message) {
$redirect = '?view=options&tab=menu';
}
} else if ($action == 'resetmenu') {
if (!canEdit('System')) {
ZM\Warning('Need System permission to reset menu items');
} else {
dbQuery('DELETE FROM Menu_Items');
dbQuery("INSERT INTO `Menu_Items` (`MenuKey`, `Enabled`, `SortOrder`) VALUES
('Console', 1, 10),
('Montage', 1, 20),
('MontageReview', 1, 30),
('Events', 1, 40),
('Options', 1, 50),
('Log', 1, 60),
('Devices', 1, 70),
('IntelGpu', 1, 80),
('Groups', 1, 90),
('Filters', 1, 100),
('Snapshots', 1, 110),
('Reports', 1, 120),
('ReportEventAudit', 1, 130),
('Map', 1, 140)");
}
$redirect = '?view=options&tab=menu';
} // end if object vs action
?>