mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
* security: consolidate verified GHSA fixes for develop (batch 4) Develop counterparts of the 1.2.x batch-4 fixes in #8144, re-verified against current develop HEAD. - GHSA-4rmr-wvjq-qxc2: include/auth.php Basic Auth and guest branches fall through to the per-page realm check instead of returning early. - GHSA-j67j-wpm4-9g3x: the pre-dispatch update_policy branch in user_admin.php and user_group_admin.php requires a CSRF-token POST. The denylist -> allowlist redesign is tracked for 1.3 in #8143. - GHSA-2p3x-f5jm-rg6p: the View-Graphs -> poller RCE chain is broken by the realm check (4rmr), the update_policy guard (j67j) and shell-free webhits (x5pw). - GHSA-x5pw-cg7j-mj4r: scripts/webhits.pl runs wc through a shell-free list-form open. - GHSA-9737-rqh3-h7fh: names interpolated into raise_message() now use __esc() across the UI; URL- and HTML-bearing messages are excluded. - GHSA-9x9h-2577-9w86: the colordropdown input is built as a node and the label is no longer round-tripped through parseHTML (the list item already used createTextNode on develop). - GHSA-h96j-8xhc-2p3w: remote_agent.php authorizes on $_SERVER['REMOTE_ADDR'], not get_client_addr(), which honours client-settable proxy headers. Regression guards under tests/Unit/Security/{Auth,Csrf,Xss,ShellInjection}. php -l, check-changelog.py and the sink/architectural baselines all pass. * chore: reference PR #8145 on the batch-4 1.3.0-dev CHANGELOG entries * security: resolve develop merge - keep id-only xmlerror unescaped (matches #8141 h745 test) * security(GHSA-j67j): use REQUEST_METHOD POST guard instead of raw \ (hardening-pattern-guard) * security(GHSA-9737): fix review findings - drop double-encoding in __esc, escape missed name sinks, correct plugins format-arg count
535 lines
18 KiB
PHP
535 lines
18 KiB
PHP
<?php
|
|
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| 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. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
| This code is designed, written, and maintained by the Cacti Group. See |
|
|
| about.php and/or the AUTHORS file for specific developer information. |
|
|
+-------------------------------------------------------------------------+
|
|
| http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
require('./include/auth.php');
|
|
require_once(CACTI_PATH_LIBRARY . '/PackageListFilter.php');
|
|
require_once(CACTI_PATH_LIBRARY . '/poller.php');
|
|
require_once(CACTI_PATH_LIBRARY . '/utility.php');
|
|
require_once(CACTI_PATH_LIBRARY . '/CactiValidator.php');
|
|
|
|
use Symfony\Component\Validator\Constraints as Assert;
|
|
|
|
$actions = [
|
|
1 => __('Delete'),
|
|
2 => __('Disable'),
|
|
3 => __('Enable'),
|
|
4 => __('Default')
|
|
];
|
|
|
|
$types = [
|
|
0 => __('GitHub Based'),
|
|
1 => __('File Based'),
|
|
2 => __('Direct URL')
|
|
];
|
|
|
|
// set default action
|
|
set_default_action();
|
|
|
|
switch (grv('action')) {
|
|
case 'save':
|
|
form_save();
|
|
|
|
break;
|
|
case 'actions':
|
|
form_actions();
|
|
|
|
break;
|
|
case 'edit':
|
|
top_header();
|
|
|
|
repo_edit();
|
|
|
|
bottom_footer();
|
|
|
|
break;
|
|
default:
|
|
top_header();
|
|
|
|
repos();
|
|
|
|
bottom_footer();
|
|
|
|
break;
|
|
}
|
|
|
|
function form_save() : void {
|
|
global $registered_cacti_names;
|
|
|
|
if (isrv('save_component_repo')) {
|
|
// ================= input validation =================
|
|
$save['id'] = CactiValidator::validateInput(gnrv('id'), 'id', [new Assert\NotBlank(), new Assert\Regex('/^[0-9]+$/')], 3);
|
|
$save['repo_type'] = CactiValidator::validateInput(gnrv('repo_type'), 'repo_type', [new Assert\NotBlank(), new Assert\Regex('/^[0-2]$/')], 3);
|
|
$save['name'] = CactiValidator::validateInput(gnrv('name'), 'name', [new Assert\NotBlank(), new Assert\Length(max: 32)], 3);
|
|
$save['repo_location'] = CactiValidator::validateInput(gnrv('repo_location'), 'repo_location', [new Assert\NotBlank(), new Assert\Length(max: 128)], 3);
|
|
$save['repo_branch'] = CactiValidator::validateInput(gnrv('repo_branch'), 'repo_branch', [new Assert\Length(max: 128)], 3);
|
|
$save['repo_api_key'] = CactiValidator::validateInput(gnrv('repo_api_key'), 'repo_api_key', [new Assert\Length(max: 128)], 3);
|
|
$save['enabled'] = (isrv('enabled') ? 'on' : '');
|
|
$save['default'] = (isrv('default') ? 'on' : '');
|
|
// ====================================================
|
|
|
|
if (!is_error_message()) {
|
|
$id = sql_save($save, 'package_repositories', 'id');
|
|
|
|
if ($id) {
|
|
if ($save['repo_type'] == 0) {
|
|
$repoloc = str_replace('github.com', 'raw.githubusercontent.com', $save['repo_location']);
|
|
|
|
$file = $repoloc . '/' . $save['repo_branch'] . '/package.manifest';
|
|
|
|
if ($save['repo_api_key'] != '') {
|
|
$options = [
|
|
'https' => [
|
|
'method' => 'GET',
|
|
'header' => 'Authorization: Bearer ' . $save['repo_api_key']
|
|
]
|
|
];
|
|
|
|
$context = stream_context_create($options);
|
|
} else {
|
|
$options = [
|
|
'https' => [
|
|
'method' => 'GET',
|
|
]
|
|
];
|
|
|
|
$context = stream_context_create($options);
|
|
}
|
|
|
|
$data = file_get_contents($file, false, $context);
|
|
|
|
if ($data != '') {
|
|
raise_message('repo_exists', __esc('The Repo \'%s\' is Reachable on GitHub.', $save['name']), MESSAGE_LEVEL_INFO);
|
|
} else {
|
|
raise_message('repo_missing', __esc('The Repo \'%s\' is NOT Reachable on GitHub or the package.manifest file is missing or it could be an invalid branch. Valid Package Locations are normally: https://github.com/Author/RepoName/.', $save['name']), MESSAGE_LEVEL_WARN);
|
|
}
|
|
} elseif ($save['repo_type'] == 2) {
|
|
$file = $save['repo_location'] . '/package.manifest';
|
|
|
|
$context = [
|
|
'ssl' => [
|
|
'verify_peer' => false,
|
|
'verify_peer_name' => false,
|
|
],
|
|
];
|
|
|
|
$data = file_get_contents($file, false, stream_context_create($context));
|
|
|
|
if ($data != '') {
|
|
raise_message('repo_exists', __esc('The Repo \'%s\' is Reachable at the URL Location.', $save['name']), MESSAGE_LEVEL_INFO);
|
|
} else {
|
|
raise_message('repo_missing', __esc('The Repo \'%s\' is NOT Reachable at the URL Location or the package.manifest file is missing.', $save['name']), MESSAGE_LEVEL_WARN);
|
|
}
|
|
} else {
|
|
$file = $save['repo_location'] . '/package.manifest';
|
|
|
|
if (file_exists($file)) {
|
|
raise_message('repo_exists', __esc('The Repo \'%s\' is Reachable on the Local Cacti Server.', $save['name']), MESSAGE_LEVEL_INFO);
|
|
} else {
|
|
raise_message('repo_missing', __esc('The Repo \'%s\' is NOT Reachable on the Local Cacti Server or the package.manifest file is missing.', $save['name']), MESSAGE_LEVEL_WARN);
|
|
}
|
|
}
|
|
|
|
raise_message(1);
|
|
} else {
|
|
raise_message(2);
|
|
}
|
|
}
|
|
}
|
|
|
|
header('Location: package_repos.php?header=false&action=edit&id=' . (empty($id) ? gnrv('id') : $id));
|
|
}
|
|
|
|
function form_actions() : void {
|
|
global $actions;
|
|
|
|
// if we are to save this form, instead of display it
|
|
if (isrv('selected_items')) {
|
|
$selected_items = PackageListFilter::decodeSelectedIds(gnrv('selected_items'));
|
|
|
|
if ($selected_items != false) {
|
|
if (gnrv('drp_action') == '1') { // delete
|
|
for ($i = 0; ($i < cacti_count($selected_items)); $i++) {
|
|
repo_remove($selected_items[$i]);
|
|
}
|
|
} elseif (gnrv('drp_action') == '2') { // disable
|
|
for ($i = 0; ($i < cacti_count($selected_items)); $i++) {
|
|
repo_disable($selected_items[$i]);
|
|
}
|
|
} elseif (gnrv('drp_action') == '3') { // enable
|
|
for ($i = 0; ($i < cacti_count($selected_items)); $i++) {
|
|
repo_enable($selected_items[$i]);
|
|
}
|
|
} elseif (gnrv('drp_action') == '4') { // default
|
|
if (cacti_sizeof($selected_items) > 1) {
|
|
// error message
|
|
} else {
|
|
for ($i = 0; ($i < cacti_count($selected_items)); $i++) {
|
|
repo_default($selected_items[$i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
header('Location: package_repos.php?header=false');
|
|
|
|
exit;
|
|
}
|
|
|
|
// setup some variables
|
|
$p_list = '';
|
|
$p_array = [];
|
|
|
|
foreach (PackageListFilter::selectedIdsFromPost() as $id) {
|
|
$p_list .= '<li>' . htmle(db_fetch_cell_prepared('SELECT name FROM package_repositories WHERE id = ?', [$id])) . '</li>';
|
|
$p_array[] = $id;
|
|
}
|
|
|
|
top_header();
|
|
|
|
form_start('package_repos.php', 'action_confirm');
|
|
|
|
html_start_box($actions[gnrv('drp_action')], '60%', false, 3, 'center', '');
|
|
|
|
if (cacti_sizeof($p_array)) {
|
|
$save_html = '';
|
|
|
|
if (gnrv('drp_action') == '1') { // delete
|
|
print "<tr>
|
|
<td class='textArea'>
|
|
<p>" . __n('Click \'Continue\' to delete the following .', 'Click \'Continue\' to delete following Package Repositories.', cacti_sizeof($p_array)) . "</p>
|
|
<div class='itemlist'><ul>$p_list</ul></div>
|
|
</td>
|
|
</tr>";
|
|
|
|
$save_html = "<button type='button' class='ui-button ui-corner-all ui-widget' onClick='cactiReturnTo()'>" . __esc('Cancel') . "</button>
|
|
<button type='submit' class='ui-button ui-corner-all ui-widget ui-state-active' title='" . __n('Delete Package Repository', 'Delete Package Repositories', cacti_sizeof($p_array)) . "'>" . __esc('Continue') . '</button>';
|
|
} elseif (gnrv('drp_action') == '2') { // disable
|
|
print "<tr>
|
|
<td class='textArea'>
|
|
<p>" . __n('Click \'Continue\' to disable the following Package Repository.', 'Click \'Continue\' to disable following Package Repositories.', cacti_sizeof($p_array)) . "</p>
|
|
<div class='itemlist'><ul>$p_list</ul></div>
|
|
</td>
|
|
</tr>";
|
|
|
|
$save_html = "<button type='button' class='ui-button ui-corner-all ui-widget' onClick='cactiReturnTo()'>" . __esc('Cancel') . "</button>
|
|
<button type='submit' class='ui-button ui-corner-all ui-widget ui-state-active' title='" . __n('Disable Package Repository', 'Disable Package Repositories', cacti_sizeof($p_array)) . "'>" . __esc('Continue') . '</button>';
|
|
} elseif (gnrv('drp_action') == '3') { // enable
|
|
print "<tr>
|
|
<td class='textArea'>
|
|
<p>" . __('Click \'Continue\' to enable the following Package Repository.', 'Click \'Continue\' to enable following Package Repositories.', cacti_sizeof($p_array)) . "</p>
|
|
<div class='itemlist'><ul>$p_list</ul></div>
|
|
</td>
|
|
</tr>";
|
|
|
|
$save_html = "<button type='button' class='ui-button ui-corner-all ui-widget' onClick='cactiReturnTo()'>" . __esc('Cancel') . "</button>
|
|
<button type='submit' class='ui-button ui-corner-all ui-widget ui-state-active' title='" . __n('Enabled Package Repository', 'Enable Package Repositories', cacti_sizeof($p_array)) . "'>" . __esc('Continue') . '</button>';
|
|
} elseif (gnrv('drp_action') == '4') { // default
|
|
print "<tr>
|
|
<td class='textArea'>
|
|
<p>" . __('Click \'Continue\' to make the following the following Package Repository the default one.') . "</p>
|
|
<div class='itemlist'><ul>$p_list</ul></div>
|
|
</td>
|
|
</tr>";
|
|
|
|
$save_html = "<button type='button' class='ui-button ui-corner-all ui-widget' onClick='cactiReturnTo()'>" . __esc('Cancel') . "</button>
|
|
<button type='submit' class='ui-button ui-corner-all ui-widget ui-state-active' title='" . __esc('Make Selected Repository Default') . "'>" . __esc('Continue') . '</button>';
|
|
}
|
|
} else {
|
|
raise_message(40);
|
|
header('Location: package_repos.php?header=false');
|
|
|
|
exit;
|
|
}
|
|
|
|
print "<tr>
|
|
<td class='saveRow'>
|
|
<input type='hidden' name='action' value='actions'>
|
|
<input type='hidden' name='selected_items' value='" . htmle(PackageListFilter::encodeSelectedIds($p_array)) . "'>
|
|
<input type='hidden' name='drp_action' value='" . htmle(gnrv('drp_action')) . "'>
|
|
$save_html
|
|
</td>
|
|
</tr>";
|
|
|
|
html_end_box();
|
|
|
|
form_end();
|
|
|
|
bottom_footer();
|
|
}
|
|
|
|
function repo_remove(int $id) : void {
|
|
db_execute_prepared('DELETE FROM package_repositories WHERE id = ?', [$id]);
|
|
db_execute_prepared('DELETE FROM package_repositories WHERE id = ?', [$id]);
|
|
}
|
|
|
|
function repo_disable(int $id) : void {
|
|
db_execute_prepared('UPDATE package_repositories SET enabled = "" WHERE id = ?', [$id]);
|
|
}
|
|
|
|
function repo_enable(int $id) : void {
|
|
db_execute_prepared('UPDATE package_repositories SET enabled = "on" WHERE id = ?', [$id]);
|
|
}
|
|
|
|
function repo_default(int $id) : void {
|
|
db_execute('UPDATE package_repositories SET `default` = ""');
|
|
db_execute_prepared('UPDATE package_repositories SET `default` = "on" WHERE id = ?', [$id]);
|
|
}
|
|
|
|
function repo_edit() : void {
|
|
global $types;
|
|
|
|
// ================= input validation =================
|
|
gfrv('id');
|
|
// ====================================================
|
|
|
|
if (!ierv('id')) {
|
|
$repo = db_fetch_row_prepared('SELECT * FROM package_repositories WHERE id = ?', [grv('id')]);
|
|
$header_label = __esc('Package Repository [edit: %s]', $repo['name']);
|
|
} else {
|
|
$header_label = __('Package Repository [new]');
|
|
$repo = [];
|
|
}
|
|
|
|
if (cacti_sizeof($repo)) {
|
|
if ($repo['repo_type'] == 1) { // Local directory
|
|
$method = 'dirpath';
|
|
} else {
|
|
$method = 'textbox';
|
|
}
|
|
} else {
|
|
$method = 'textbox';
|
|
}
|
|
|
|
$fields_package = [
|
|
'name' => [
|
|
'method' => 'textbox',
|
|
'friendly_name' => __('Name'),
|
|
'description' => __('Enter a meaningful name for this Package Repository.'),
|
|
'value' => '|arg1:name|',
|
|
'max_length' => '32',
|
|
],
|
|
'repo_type' => [
|
|
'method' => 'drop_array',
|
|
'friendly_name' => __('Repository Type'),
|
|
'description' => __('Choose what Package Repository type this is.'),
|
|
'value' => '|arg1:repo_type|',
|
|
'array' => $types,
|
|
'default' => '0'
|
|
],
|
|
'repo_location' => [
|
|
'method' => $method,
|
|
'friendly_name' => __('Location'),
|
|
'description' => __('Either the full path on the Cacti Web Server to the Repository or a URL to the Repository. For example: https://github.com/Cacti/packages/'),
|
|
'value' => '|arg1:repo_location|',
|
|
'max_length' => '128',
|
|
'size' => '120'
|
|
],
|
|
'repo_branch' => [
|
|
'method' => 'textbox',
|
|
'friendly_name' => __('Branch'),
|
|
'description' => __('For GitHub based repositories, the branch to include such as \'develop\', \'master\', etc.'),
|
|
'value' => '|arg1:repo_branch|',
|
|
'max_length' => '128',
|
|
'size' => '10'
|
|
],
|
|
'repo_api_key' => [
|
|
'method' => 'textbox',
|
|
'friendly_name' => __('API Key (optional)'),
|
|
'description' => __('For GitHub based repositories, the optional API key required to access the repository.'),
|
|
'value' => '|arg1:repo_api_key|',
|
|
'max_length' => '128',
|
|
'size' => '100'
|
|
],
|
|
'enabled' => [
|
|
'method' => 'checkbox',
|
|
'friendly_name' => __('Enabled'),
|
|
'description' => __('If this checkbox is checked, you will be able to import packages from this Repository.'),
|
|
'value' => '|arg1:enabled|',
|
|
'default' => 'on',
|
|
],
|
|
'default' => [
|
|
'method' => 'checkbox',
|
|
'friendly_name' => __('Default'),
|
|
'description' => __('If this checkbox is checked, this will be the first Repository shown to the user.'),
|
|
'value' => '|arg1:default|',
|
|
'default' => '',
|
|
],
|
|
'id' => [
|
|
'method' => 'hidden_zero',
|
|
'value' => '|arg1:id|'
|
|
],
|
|
'save_component_repo' => [
|
|
'method' => 'hidden',
|
|
'value' => '1'
|
|
]
|
|
];
|
|
|
|
form_start('package_repos.php');
|
|
|
|
html_start_box($header_label, '100%', true, 3, 'center', '');
|
|
|
|
draw_edit_form(
|
|
[
|
|
'config' => [],
|
|
'fields' => inject_form_variables($fields_package, $repo)
|
|
]
|
|
);
|
|
|
|
html_end_box(true);
|
|
|
|
?>
|
|
<script type='text/javascript'>
|
|
function initPackageType() {
|
|
if ($('#repo_type').val() == 0) {
|
|
$('#row_repo_branch').show();
|
|
$('#row_repo_api_key').show();
|
|
} else {
|
|
$('#row_repo_branch').hide();
|
|
$('#row_repo_api_key').hide();
|
|
}
|
|
}
|
|
|
|
$(function() {
|
|
initPackageType();
|
|
|
|
$('#repo_type').change(function() {
|
|
initPackageType();
|
|
});
|
|
});
|
|
</script>
|
|
<?php
|
|
|
|
form_save_button('package_repos.php', 'return', 'id');
|
|
}
|
|
|
|
function repos() : void {
|
|
global $actions, $item_rows, $types;
|
|
|
|
// create the page filter
|
|
$pageFilter = new CactiTableFilter(__('Package Repositories'), 'package_repos.php', 'fors', 'sess_package_repos', 'package_repos.php?action=edit');
|
|
|
|
$pageFilter->rows_label = __('Repos');
|
|
$pageFilter->render();
|
|
|
|
$filter = PackageListFilter::fromRequest((int) read_config_option('num_rows_table'));
|
|
|
|
$sql_where = '';
|
|
$sql_params = [];
|
|
|
|
// form the 'where' clause for our main sql query
|
|
if ($filter->hasFilter()) {
|
|
$sql_where = ($sql_where != '' ? ' AND ' : 'WHERE ') .
|
|
'(name LIKE ? OR repo_branch LIKE ? OR repo_location LIKE ?)';
|
|
|
|
$sql_params[] = '%' . $filter->filter() . '%';
|
|
$sql_params[] = '%' . $filter->filter() . '%';
|
|
$sql_params[] = '%' . $filter->filter() . '%';
|
|
}
|
|
|
|
$sql_order = get_order_string();
|
|
$sql_limit = ' LIMIT ' . $filter->offset() . ',' . $filter->rows();
|
|
|
|
$total_rows = db_fetch_cell_prepared("SELECT COUNT(*)
|
|
FROM package_repositories
|
|
$sql_where",
|
|
$sql_params);
|
|
|
|
$repos = db_fetch_assoc_prepared("SELECT *
|
|
FROM package_repositories
|
|
$sql_where
|
|
$sql_order
|
|
$sql_limit",
|
|
$sql_params);
|
|
|
|
$display_text = [
|
|
'name' => [
|
|
'display' => __('Name'),
|
|
'sort' => 'ASC'
|
|
],
|
|
'repo_type' => [
|
|
'display' => __('Type'),
|
|
'sort' => 'ASC'
|
|
],
|
|
'repo_location' => [
|
|
'display' => __('Location'),
|
|
'sort' => 'ASC'
|
|
],
|
|
'repo_branch' => [
|
|
'display' => __('Branch'),
|
|
'align' => 'center',
|
|
'sort' => 'ASC'
|
|
],
|
|
'enabled' => [
|
|
'display' => __('Enabled'),
|
|
'align' => 'center',
|
|
'sort' => 'ASC'
|
|
],
|
|
'default' => [
|
|
'display' => __('Default'),
|
|
'align' => 'center',
|
|
'sort' => 'ASC'
|
|
],
|
|
];
|
|
|
|
$nav = html_nav_bar($filter->paginationUrl('package_repos.php'), MAX_DISPLAY_PAGES, $filter->page(), $filter->rows(), $total_rows, cacti_sizeof($display_text) + 1, __('Package Repositories'), 'page', 'main');
|
|
|
|
form_start('package_repos.php', 'chk');
|
|
|
|
print $nav;
|
|
|
|
html_start_box('', '100%', false, 3, 'center', '');
|
|
|
|
html_header_sort_checkbox($display_text, $filter->sortColumn(), $filter->sortDirection(), false);
|
|
|
|
if (cacti_sizeof($repos)) {
|
|
foreach ($repos as $repo) {
|
|
form_alternate_row('line' . $repo['id'], true);
|
|
|
|
form_selectable_cell(filter_value($repo['name'], $filter->filter(), 'package_repos.php?action=edit&id=' . $repo['id']), $repo['id']);
|
|
form_selectable_cell($types[$repo['repo_type']], $repo['id']);
|
|
form_selectable_cell(filter_value($repo['repo_location'], $filter->filter()), $repo['id']);
|
|
form_selectable_ecell($repo['repo_type'] == 0 ? ($repo['repo_branch'] != '' ? $repo['repo_branch'] : __('default')) : __('N/A'), $repo['id'], '', 'center');
|
|
form_selectable_cell($repo['enabled'] == 'on' ? __('Yes') : __('No'), $repo['id'], '', 'center');
|
|
form_selectable_cell($repo['default'] == 'on' ? __('Yes') : __('No'), $repo['id'], '', 'center');
|
|
|
|
form_checkbox_cell($repo['name'], $repo['id']);
|
|
|
|
form_end_row();
|
|
}
|
|
} else {
|
|
print '<tr class="tableRow odd"><td colspan="' . (cacti_sizeof($display_text) + 1) . '"><em>' . __('No Package Repositories Found') . '</em></td></tr>';
|
|
}
|
|
|
|
html_end_box(false);
|
|
|
|
if (cacti_sizeof($repos)) {
|
|
print $nav;
|
|
}
|
|
|
|
// draw the dropdown containing a list of available actions for this form
|
|
draw_actions_dropdown($actions);
|
|
|
|
form_end();
|
|
}
|