mirror of
https://github.com/webmin/webmin.git
synced 2026-10-24 13:10:40 +01:00
1309 lines
47 KiB
Perl
Executable File
1309 lines
47 KiB
Perl
Executable File
# sqlite-lib.pl
|
|
# Shared functions for browsing and editing SQLite databases.
|
|
|
|
BEGIN { push(@INC, ".."); }
|
|
use WebminCore;
|
|
use strict;
|
|
use warnings;
|
|
use bytes ();
|
|
use Encode qw(decode FB_CROAK);
|
|
use MIME::Base64 ();
|
|
use POSIX ();
|
|
|
|
our (%config, %access, %text, %module_info, $module_name,
|
|
$module_config_directory, $remote_user);
|
|
our ($sqlite_driver_loaded, $sqlite_driver_error, $sqlite_unix_home);
|
|
|
|
# Load Webmin configuration unless a test provides isolated globals, then
|
|
# drop root for the Unix user the ACL names before any file is touched.
|
|
if (!$ENV{'SQLITE_TESTING'}) {
|
|
&init_config();
|
|
%access = &get_module_acl();
|
|
&sqlite_switch_user();
|
|
}
|
|
|
|
# sub sqlite_work_as_user([&acl])
|
|
# Returns the Unix user that file and database work should run as, or
|
|
# undef to stay root: a named user from the ACL, the Webmin user's own
|
|
# account when the ACL says so, or for an ACL written before the setting
|
|
# existed, that account when the ACL allows only the user's home.
|
|
sub sqlite_work_as_user
|
|
{
|
|
my ($acl) = @_;
|
|
$acl ||= \%access;
|
|
# Honor explicit root and named-user modes before checking legacy ACLs.
|
|
return undef if ($acl->{'work_as_root'});
|
|
return $acl->{'work_as_user'} if ($acl->{'work_as_user'});
|
|
# An older ACL has no setting; only a home-only one implies the user.
|
|
if (!defined($acl->{'work_as_root'})) {
|
|
my @paths = grep { length($_) }
|
|
split(/\t+|\r?\n/, defined($acl->{'paths'}) ?
|
|
$acl->{'paths'} : '');
|
|
return undef if (!@paths || grep { $_ ne '$HOME' } @paths);
|
|
}
|
|
# The remaining explicit or inferred mode uses the matching Webmin user.
|
|
return $remote_user;
|
|
}
|
|
|
|
# sub sqlite_switch_user()
|
|
# Switches the process to the Unix user from sqlite_work_as_user, so that
|
|
# the kernel enforces every file permission on database files and nothing
|
|
# in a user's directory is done as root. Does nothing when the module is
|
|
# to stay root, already runs without root, or was loaded by another
|
|
# module's CGI, such as the ACL editor, which must keep its own identity.
|
|
sub sqlite_switch_user
|
|
{
|
|
return if ($< != 0);
|
|
return if (($ENV{'SCRIPT_NAME'} || '') !~ m!^/\Q$module_name\E/!);
|
|
my $user = &sqlite_work_as_user();
|
|
return if (!defined($user) || $user eq '');
|
|
my @uinfo = getpwnam($user);
|
|
@uinfo || &error(&text('error_nouser', &html_escape($user)));
|
|
&switch_to_unix_user(\@uinfo);
|
|
# Tell the rest of Webmin, and programs run later, who we are now.
|
|
$ENV{'USER'} = $ENV{'LOGNAME'} = $uinfo[0];
|
|
$ENV{'HOME'} = $uinfo[7];
|
|
@WebminCore::remote_user_info = @uinfo;
|
|
$sqlite_unix_home = $uinfo[7];
|
|
}
|
|
|
|
# sub sqlite_acl_paths(value)
|
|
# Returns allowed database directories, expanding $HOME for the Unix user.
|
|
# Keep explicit ACL paths fixed after saving them. Resolve $HOME so
|
|
# administrator-managed links such as /home -> /data continue to work.
|
|
sub sqlite_acl_paths
|
|
{
|
|
my ($value) = @_;
|
|
# An unset ACL must grant no filesystem access.
|
|
return () if (!defined($value) || $value eq '');
|
|
my @paths = grep { length($_) } split(/\t+|\r?\n/, $value);
|
|
# $HOME belongs to the Unix user the module works as, or otherwise to the
|
|
# Unix account named like the Webmin user.
|
|
my $owner = &sqlite_work_as_user() || $remote_user;
|
|
my @user = $owner ? getpwnam($owner) : ( );
|
|
my @resolved;
|
|
foreach my $path (@paths) {
|
|
# $HOME is valid only when the ACL identifies a matching Unix user.
|
|
if ($path eq '$HOME') {
|
|
$path = $user[7] ?
|
|
&simplify_path(&resolve_links($user[7])) : undef;
|
|
}
|
|
else {
|
|
# The ACL editor stores canonical paths. Normalize lexical components
|
|
# without following a link that may have been replaced later.
|
|
$path = &simplify_path($path);
|
|
}
|
|
# Discard unresolved or non-absolute ACL entries.
|
|
push(@resolved, $path) if ($path && $path =~ m!^/!);
|
|
}
|
|
return @resolved;
|
|
}
|
|
|
|
# sub sqlite_load_driver()
|
|
# Loads DBI and DBD::SQLite. Returns an error string on failure.
|
|
sub sqlite_load_driver
|
|
{
|
|
# Reuse the first result for the rest of the request.
|
|
return undef if ($sqlite_driver_loaded);
|
|
return $sqlite_driver_error if (defined($sqlite_driver_error));
|
|
|
|
# Load both modules and initialize the SQLite driver immediately.
|
|
eval {
|
|
require DBI;
|
|
require DBD::SQLite;
|
|
DBI->install_driver('SQLite');
|
|
};
|
|
# Save the loader error for the installation prompt and later calls.
|
|
if ($@) {
|
|
$sqlite_driver_error = $@;
|
|
$sqlite_driver_error =~ s/\s+$//;
|
|
return $sqlite_driver_error;
|
|
}
|
|
$sqlite_driver_loaded = 1;
|
|
return undef;
|
|
}
|
|
|
|
# sub sqlite_result_limit_error([dbh])
|
|
# Returns undef when the driver provides sqlite_limit. Otherwise, returns an
|
|
# error that names the installed version. When supplied, dbh is checked before
|
|
# the driver class. Backported support is accepted.
|
|
sub sqlite_result_limit_error
|
|
{
|
|
my ($dbh) = @_;
|
|
my $driver_error = &sqlite_load_driver();
|
|
# Propagate a driver load failure before checking its methods.
|
|
return $driver_error if ($driver_error);
|
|
my $class = 'DBD::SQLite::db';
|
|
# Accept the method on either the supplied handle or the loaded driver class.
|
|
return undef if ($dbh ? $dbh->can('sqlite_limit') :
|
|
$class->can('sqlite_limit'));
|
|
my (undef, $version) = &sqlite_versions();
|
|
# Keep the compatibility error useful when the driver reports no version.
|
|
$version = $text{'unknown'} || 'unknown' if (!defined($version));
|
|
return "DBD::SQLite $version does not provide sqlite_limit";
|
|
}
|
|
|
|
# sub sqlite_versions()
|
|
# Returns the SQLite library version and the DBD::SQLite version once the
|
|
# driver is loaded, or an empty list.
|
|
sub sqlite_versions
|
|
{
|
|
return () if (!$sqlite_driver_loaded);
|
|
no warnings 'once';
|
|
return ($DBD::SQLite::sqlite_version, $DBD::SQLite::VERSION);
|
|
}
|
|
|
|
# sub sqlite_path_under(path, directory)
|
|
# Returns true when a canonical candidate path is the saved ACL directory or
|
|
# one of its descendants. Callers resolve candidate links but keep saved ACL
|
|
# roots unchanged.
|
|
sub sqlite_path_under
|
|
{
|
|
my ($path, $dir) = @_;
|
|
$path = &simplify_path($path);
|
|
$dir = &simplify_path($dir);
|
|
# Reject paths that cannot be normalized.
|
|
return 0 if (!defined($path) || !defined($dir));
|
|
# A root ACL includes every absolute path.
|
|
return 1 if ($dir eq '/');
|
|
# Accept the configured directory itself as well as its descendants.
|
|
return 1 if ($path eq $dir);
|
|
$dir =~ s!/+$!!;
|
|
return index($path, $dir.'/') == 0;
|
|
}
|
|
|
|
# sub sqlite_acl_unrestricted()
|
|
# Returns true when the ACL includes the filesystem root.
|
|
sub sqlite_acl_unrestricted
|
|
{
|
|
foreach my $dir (&sqlite_acl_paths($access{'paths'})) {
|
|
# Only the exact root path grants unrestricted filesystem access.
|
|
return 1 if ($dir eq '/');
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
# sub sqlite_secure_open_available([sqlite-version])
|
|
# Returns true when the active ACL can be enforced while opening a validated
|
|
# database path. sqlite-version overrides the loaded version for tests. A
|
|
# restricted ACL requires SQLite 3.39 or later to reject links in every path
|
|
# component.
|
|
sub sqlite_secure_open_available
|
|
{
|
|
my ($version) = @_;
|
|
# An unrestricted ACL has no narrower directory boundary to escape.
|
|
return 1 if (&sqlite_acl_unrestricted());
|
|
return &sqlite_supports('nofollow', $version);
|
|
}
|
|
|
|
# sub sqlite_path_allowed(path)
|
|
# Returns true when a path lies inside one of the ACL's allowed directories.
|
|
sub sqlite_path_allowed
|
|
{
|
|
my ($path) = @_;
|
|
foreach my $dir (&sqlite_acl_paths($access{'paths'})) {
|
|
# Stop at the first ACL root that contains the path.
|
|
return 1 if (&sqlite_path_under($path, $dir));
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
# sub sqlite_database_path(path, [create])
|
|
# Resolves and validates a database path against the module ACL. Set create
|
|
# to permit a missing final file when its parent directory is writable.
|
|
sub sqlite_database_path
|
|
{
|
|
my ($path, $create) = @_;
|
|
# Normalize an omitted form value before validating it.
|
|
$path = '' if (!defined($path));
|
|
$path =~ s/^\s+|\s+$//g;
|
|
# Require an absolute, single-line path before any filesystem access.
|
|
if ($path !~ m!^/! || $path =~ /[\0\r\n]/) {
|
|
&error(&text('error_path', &html_escape($path)));
|
|
}
|
|
|
|
my $simple = &simplify_path($path);
|
|
defined($simple) || &error(&text('error_path', &html_escape($path)));
|
|
# Resolve links before applying directory boundaries, file checks or errors.
|
|
my $resolved = &simplify_path(&resolve_links($simple));
|
|
defined($resolved) || &error(&text('error_path', &html_escape($path)));
|
|
|
|
# Apply the ACL before any file metadata error, so paths outside the
|
|
# allowed directories cannot be probed for existence or type. A rejection
|
|
# names the ACL roots, escaped so that ACL text cannot become HTML.
|
|
if (!&sqlite_path_allowed($resolved)) {
|
|
my @allowed_paths = &sqlite_acl_paths($access{'paths'});
|
|
my $allowed_text = @allowed_paths ?
|
|
join(', ', map { &ui_tag('tt', &html_escape($_)) }
|
|
@allowed_paths) :
|
|
&html_escape($text{'error_noallowed'});
|
|
&error(&text('error_notallowed', &html_escape($resolved),
|
|
$allowed_text));
|
|
}
|
|
|
|
# Validate an existing file, or validate the parent before creating a new one.
|
|
if (-e $resolved) {
|
|
# Existing databases must be readable regular files.
|
|
-f $resolved || &error(&text('error_notfile', &html_escape($resolved)));
|
|
-r $resolved || &error(&text('error_noread', &html_escape($resolved)));
|
|
}
|
|
else {
|
|
# New files require create permission and a writable parent directory.
|
|
$create || &error(&text('error_missing', &html_escape($resolved)));
|
|
$access{'create'} || &error($text{'error_nocreate'});
|
|
my ($parent, $name) = $resolved =~ m!^(.*)/([^/]+)$!;
|
|
# A path directly below the filesystem root captures an empty parent.
|
|
$parent = '/' if ($parent eq '');
|
|
$name ne '' || &error(&text('error_path', &html_escape($path)));
|
|
-d $parent || &error(&text('error_noparent', &html_escape($parent)));
|
|
-w $parent || &error(&text('error_nowrite', &html_escape($parent)));
|
|
}
|
|
return $resolved;
|
|
}
|
|
|
|
# sub sqlite_can_create()
|
|
# Returns true when this user may create database files: the ACL allows
|
|
# it and the user can give the new file a table, through the table form
|
|
# or through SQL. Otherwise a new file would be a dead end.
|
|
sub sqlite_can_create
|
|
{
|
|
return $access{'create'} && ($access{'write'} || $access{'sql'}) ? 1 : 0;
|
|
}
|
|
|
|
# sub sqlite_can_write(path)
|
|
# Returns true when ACLs and filesystem permissions allow database changes.
|
|
sub sqlite_can_write
|
|
{
|
|
my ($path) = @_;
|
|
# The module ACL takes precedence over filesystem permissions.
|
|
return 0 if (!$access{'write'});
|
|
return &sqlite_file_writable($path);
|
|
}
|
|
|
|
# sub sqlite_can_sql_write(path)
|
|
# Returns true when direct SQL may change a database and the filesystem
|
|
# permits the change. Structured row and schema controls use sqlite_can_write
|
|
# instead, so their separate ACL remains effective.
|
|
sub sqlite_can_sql_write
|
|
{
|
|
my ($path) = @_;
|
|
return 0 if (!$access{'sql'});
|
|
return &sqlite_file_writable($path);
|
|
}
|
|
|
|
# sub sqlite_file_writable(path)
|
|
# Returns true when SQLite can replace the database and create its journal
|
|
# files. Callers must apply the relevant module capability first.
|
|
sub sqlite_file_writable
|
|
{
|
|
my ($path) = @_;
|
|
my ($parent) = $path =~ m!^(.*)/[^/]+$!;
|
|
# Files directly below the filesystem root have root as their parent.
|
|
$parent = '/' if (!defined($parent) || $parent eq '');
|
|
return (-w $path && -w $parent) ? 1 : 0;
|
|
}
|
|
|
|
# sub sqlite_filename_uri(path)
|
|
# Encodes an absolute filename as an SQLite file URI. The URI form keeps
|
|
# semicolons and equals signs in a Unix filename from becoming DBD connection
|
|
# parameters such as a second dbname.
|
|
sub sqlite_filename_uri
|
|
{
|
|
my ($path) = @_;
|
|
my $bytes = $path;
|
|
# URI percent escapes describe filename bytes, not Perl characters.
|
|
utf8::encode($bytes) if (utf8::is_utf8($bytes));
|
|
$bytes =~ s!([^A-Za-z0-9._~/-])!sprintf('%%%02X', ord($1))!ge;
|
|
return 'file:'.$bytes;
|
|
}
|
|
|
|
# sub sqlite_result_bytes_limit()
|
|
# Returns the configured result-data budget, clamped from 1 MiB to 64 MiB.
|
|
sub sqlite_result_bytes_limit
|
|
{
|
|
my $limit = defined($config{'max_result_bytes'}) &&
|
|
$config{'max_result_bytes'} ne '' ?
|
|
int($config{'max_result_bytes'}) : 16777216;
|
|
# Very small budgets would reject ordinary schema metadata.
|
|
$limit = 1048576 if ($limit < 1048576);
|
|
# Keep one request from retaining more than 64 MiB of result data.
|
|
$limit = 67108864 if ($limit > 67108864);
|
|
return $limit;
|
|
}
|
|
|
|
# sub sqlite_with_value_limit(dbh, &code)
|
|
# Runs code in list context while capping SQLite values at 64 MiB, then
|
|
# restores the previous limit even if the callback dies. Missing limit control
|
|
# and oversized values use localized errors. Returns every callback value to a
|
|
# list caller, or the first value to a scalar caller.
|
|
sub sqlite_with_value_limit
|
|
{
|
|
my ($dbh, $code) = @_;
|
|
my $limit_error = &sqlite_result_limit_error($dbh);
|
|
# Refuse the operation when the driver cannot enforce the ceiling.
|
|
die &text('error_resultcontrol', $limit_error)."\n" if ($limit_error);
|
|
# SQLITE_LIMIT_LENGTH is category zero in SQLite's stable public API.
|
|
my $old = $dbh->sqlite_limit(0, -1);
|
|
my $ceiling = $old < 67108864 ? $old : 67108864;
|
|
$dbh->sqlite_limit(0, $ceiling);
|
|
# Invoke the callback in list context so it can return several result fields.
|
|
my @rv = eval { $code->() };
|
|
my $error = $@;
|
|
# Preserve a callback error; otherwise report failure to restore the old limit.
|
|
eval { $dbh->sqlite_limit(0, $old); 1 } || ($error ||= $@);
|
|
# Translate the hard-ceiling error and preserve every other exception.
|
|
if ($error) {
|
|
# Give SQLite's oversized-value error a stable, localized explanation.
|
|
die &text('error_valuelarge', $ceiling)."\n"
|
|
if ($error =~ /string or blob too big/i);
|
|
die $error;
|
|
}
|
|
return wantarray ? @rv : $rv[0];
|
|
}
|
|
|
|
# sub sqlite_fetch_rows(sth, [row-limit])
|
|
# Fetches rows from an executed statement until it ends, the row limit is
|
|
# reached, or the next row would push the retained data past the configured
|
|
# byte budget. Returns the rows and why fetching stopped early: 'rows',
|
|
# 'bytes', or undef when every row was fetched.
|
|
sub sqlite_fetch_rows
|
|
{
|
|
my ($sth, $limit) = @_;
|
|
my $byte_limit = &sqlite_result_bytes_limit();
|
|
my ($bytes, $stopped, @rows) = (0);
|
|
# Copy rows until the result ends, the row cap, or the byte budget is reached.
|
|
while (my $row = $sth->fetchrow_arrayref()) {
|
|
# One extra row distinguishes a complete result from row truncation.
|
|
if ($limit && @rows >= $limit) {
|
|
$stopped = 'rows';
|
|
last;
|
|
}
|
|
my $copy = [ @$row ];
|
|
$bytes += &sqlite_row_bytes($copy);
|
|
# Stop before retaining a row that crosses the aggregate byte budget.
|
|
if ($bytes > $byte_limit) {
|
|
$stopped = 'bytes';
|
|
last;
|
|
}
|
|
push(@rows, $copy);
|
|
}
|
|
return (\@rows, $stopped);
|
|
}
|
|
|
|
# sub sqlite_sum_sql(expressions...)
|
|
# Builds a balanced SQL addition expression so wide schemas do not exceed
|
|
# SQLite's expression-depth limit.
|
|
sub sqlite_sum_sql
|
|
{
|
|
my (@parts) = @_;
|
|
# An empty list sums to SQL zero.
|
|
return '0' if (!@parts);
|
|
# Pair expressions until one balanced tree remains.
|
|
while (@parts > 1) {
|
|
my @next;
|
|
for(my $i = 0; $i < @parts; $i += 2) {
|
|
my $left = $parts[$i];
|
|
my $right = $i + 1 < @parts ? $parts[$i + 1] : undef;
|
|
push(@next, defined($right) ? "($left + $right)" : $left);
|
|
}
|
|
@parts = @next;
|
|
}
|
|
return $parts[0];
|
|
}
|
|
|
|
# sub sqlite_row_bytes(&row)
|
|
# Returns a conservative estimate of one retained result row, including
|
|
# storage for the row and its scalar slots as well as the value bytes.
|
|
sub sqlite_row_bytes
|
|
{
|
|
my ($row) = @_;
|
|
my $bytes = 64 + 64 * scalar(@$row);
|
|
foreach my $value (@$row) {
|
|
# References are metadata whose slot overhead was counted above.
|
|
$bytes += bytes::length($value) if (defined($value) && !ref($value));
|
|
}
|
|
return $bytes;
|
|
}
|
|
|
|
# sub sqlite_select_rows_bounded(dbh, sql, [parameters...])
|
|
# Runs a query whose complete result must fit the request budget, such as
|
|
# schema metadata or one page of a table, under the value ceiling. Dies with
|
|
# the result-too-large message when it does not fit, since a partial result
|
|
# would be unsafe to act on.
|
|
sub sqlite_select_rows_bounded
|
|
{
|
|
my ($dbh, $sql, @params) = @_;
|
|
# sub bounded_query()
|
|
# Executes and drains the internal statement under the temporary value limit.
|
|
my ($rows, $stopped) = &sqlite_with_value_limit($dbh, sub {
|
|
my $sth = $dbh->prepare($sql);
|
|
$sth->execute(@params);
|
|
my @fetched = &sqlite_fetch_rows($sth);
|
|
$sth->finish();
|
|
return @fetched;
|
|
});
|
|
# A partial internal result is unsafe for metadata and editing decisions.
|
|
die &text('error_resultlarge', &sqlite_result_bytes_limit())."\n"
|
|
if ($stopped);
|
|
return $rows;
|
|
}
|
|
|
|
# sub sqlite_select_metadata_rows(dbh, sql, [parameters...])
|
|
# Runs a bounded metadata query with optional bind values. Converts query
|
|
# failures into escaped Webmin error pages instead of uncaught CGI exceptions.
|
|
sub sqlite_select_metadata_rows
|
|
{
|
|
my ($dbh, $sql, @params) = @_;
|
|
my $rows = eval { &sqlite_select_rows_bounded($dbh, $sql, @params) };
|
|
my $error = $@;
|
|
# Send any query failure to a standard escaped Webmin error page.
|
|
&error(&html_escape($error)) if ($error);
|
|
return $rows;
|
|
}
|
|
|
|
# sub sqlite_bytes_sql("ed-columns, [count-blobs])
|
|
# Returns an SQL expression for the total byte length of the column values.
|
|
# BLOBs count only when count-blobs is set because display queries replace
|
|
# their bodies with NULL.
|
|
sub sqlite_bytes_sql
|
|
{
|
|
my ($quoted, $count_blobs) = @_;
|
|
return &sqlite_sum_sql(map {
|
|
my $bytes = "coalesce(length(CAST($_ AS BLOB)), 0)";
|
|
$count_blobs ? $bytes :
|
|
"CASE WHEN typeof($_) = 'blob' THEN 0 ELSE $bytes END"
|
|
} @$quoted);
|
|
}
|
|
|
|
# sub sqlite_guarded_select(size-sql, max-bytes, &plain, &guarded, from-sql)
|
|
# Builds a SELECT that returns a fit flag, plain expressions and size-guarded
|
|
# expressions. SQLite's lazy CASE evaluation skips guarded expressions when
|
|
# size-sql exceeds max-bytes. from-sql supplies the FROM clause and any WHERE,
|
|
# ORDER BY, LIMIT or OFFSET clauses.
|
|
sub sqlite_guarded_select
|
|
{
|
|
my ($size, $max, $plain, $guarded, $from) = @_;
|
|
my @select = ('('.$size.') <= '.$max, @$plain);
|
|
# Guard full values directly so a subquery cannot evaluate them first.
|
|
push(@select, map {
|
|
'CASE WHEN ('.$size.') <= '.$max.' THEN '.$_.' ELSE NULL END'
|
|
} @$guarded);
|
|
return 'SELECT '.join(', ', @select).' '.$from;
|
|
}
|
|
|
|
# sub sqlite_connect(path, [write], [create], [sql-write])
|
|
# Opens a database using SQLite's read-only or read-write connection mode.
|
|
# Set write for a read-write handle, create to allow a missing file, and
|
|
# sql-write when the SQL ACL authorizes the write instead of the structured
|
|
# editing ACL.
|
|
sub sqlite_connect
|
|
{
|
|
my ($path, $write, $create, $sql_write) = @_;
|
|
$path = &sqlite_database_path($path, $create);
|
|
# An existing path always uses normal open semantics, even if create was set.
|
|
$create = 0 if (-e $path);
|
|
my $driver_error = &sqlite_load_driver();
|
|
$driver_error && &error(&text('error_driver', &html_escape($driver_error)));
|
|
# Refuse to open the database unless the driver can bound values returned by
|
|
# its schema and later queries.
|
|
my $limit_error = &sqlite_result_limit_error();
|
|
$limit_error && &error(&text('error_resultcontrol',
|
|
&html_escape($limit_error)));
|
|
# Protect every restricted path ACL against an ancestor symlink race.
|
|
&sqlite_secure_open_available() || &error($text{'error_secureopen'});
|
|
# Refuse writes denied by the applicable ACL or filesystem.
|
|
if ($write && !$create && !($sql_write ? &sqlite_can_sql_write($path) :
|
|
&sqlite_can_write($path))) {
|
|
&error($text{'error_readonly'});
|
|
}
|
|
|
|
# SQLite open flags: 1 is read-only, 2 is read-write, and 6 also permits
|
|
# creation. DBD::SQLite adds the URI flag when it parses the uri= DSN.
|
|
my $flags = $create ? 6 : $write ? 2 : 1;
|
|
# The validated path holds no symbolic link, so refuse one planted there
|
|
# between validation and the open, where SQLite can check for it.
|
|
$flags |= 0x01000000 if (&sqlite_supports('nofollow'));
|
|
my %attributes = (
|
|
'RaiseError' => 1,
|
|
'PrintError' => 0,
|
|
'AutoCommit' => 1,
|
|
'sqlite_unicode' => 1,
|
|
'sqlite_open_flags' => $flags,
|
|
'sqlite_allow_multiple_statements' => 0,
|
|
);
|
|
# Defensive mode blocks SQL features that can directly corrupt the database.
|
|
$attributes{'sqlite_defensive'} = 1
|
|
if (eval { DBD::SQLite->VERSION(1.64); 1 });
|
|
my $dbh = eval {
|
|
DBI->connect('dbi:SQLite:uri='.&sqlite_filename_uri($path), '', '',
|
|
\%attributes);
|
|
};
|
|
# Report driver exceptions through Webmin's escaped error page.
|
|
if (!$dbh) {
|
|
my $message = $@ || DBI->errstr || $text{'error_unknown'};
|
|
&error(&text('error_connect', &html_escape($path),
|
|
&html_escape($message)));
|
|
}
|
|
my $timeout = int($config{'busy_timeout'} || 5000);
|
|
# A negative timeout means fail immediately.
|
|
$timeout = 0 if ($timeout < 0);
|
|
# Cap lock waits at ten minutes.
|
|
$timeout = 600000 if ($timeout > 600000);
|
|
$dbh->do('PRAGMA busy_timeout = '.$timeout);
|
|
# Enforce foreign keys, and refuse functions and virtual tables that a
|
|
# schema from an untrusted file could otherwise run.
|
|
$dbh->do('PRAGMA foreign_keys = ON');
|
|
$dbh->do('PRAGMA trusted_schema = OFF');
|
|
# Enforce query-only behavior inside SQLite as well as through open flags.
|
|
$dbh->do('PRAGMA query_only = ON') if (!$write && !$create);
|
|
# Read the schema through the bounded helper so invalid schema fails now.
|
|
eval { &sqlite_select_rows_bounded($dbh, 'PRAGMA schema_version'); };
|
|
# Close an unusable handle before reporting the error.
|
|
if ($@) {
|
|
my $message = $@;
|
|
$dbh->disconnect();
|
|
&error(&text('error_connect', &html_escape($path),
|
|
&html_escape($message)));
|
|
}
|
|
return $dbh;
|
|
}
|
|
|
|
# sub sqlite_adopt_ownership(path)
|
|
# Gives SQLite journal files the same owner as the database. When Webmin
|
|
# writes as root, a root-owned -wal or -shm file can lock out the application
|
|
# that owns the database.
|
|
sub sqlite_adopt_ownership
|
|
{
|
|
my ($path) = @_;
|
|
# Only root can hand files to another user, and root's own stay as is.
|
|
return if ($< != 0);
|
|
my @st = stat($path);
|
|
return if (!@st || $st[4] == 0);
|
|
foreach my $suffix ('-journal', '-wal', '-shm') {
|
|
# lchown does not follow a link planted after the file-type check.
|
|
&sqlite_safe_chown($path.$suffix, $st[4], $st[5]);
|
|
}
|
|
}
|
|
|
|
# sub sqlite_safe_chown(path, uid, gid)
|
|
# Gives a regular file to one user and group without following a symbolic
|
|
# link. Returns true on success and false when the target cannot be changed
|
|
# safely.
|
|
sub sqlite_safe_chown
|
|
{
|
|
my ($path, $uid, $gid) = @_;
|
|
my @st = lstat($path);
|
|
# Refuse links and every other non-regular file before changing ownership.
|
|
return 0 if (!@st || !-f _);
|
|
# A replacement link is still safe because lchown changes the link itself.
|
|
my $rv = eval { POSIX::lchown($uid, $gid, $path) };
|
|
return defined($rv) && $rv == 0 ? 1 : 0;
|
|
}
|
|
|
|
# sub sqlite_quote_identifier(name)
|
|
# Quotes an SQLite table or column identifier.
|
|
sub sqlite_quote_identifier
|
|
{
|
|
my ($name) = @_;
|
|
$name =~ s/"/""/g;
|
|
return '"'.$name.'"';
|
|
}
|
|
|
|
# sub sqlite_objects(dbh)
|
|
# Returns user-created tables and views from an open database.
|
|
sub sqlite_objects
|
|
{
|
|
my ($dbh) = @_;
|
|
my $rows = &sqlite_select_metadata_rows($dbh,
|
|
"SELECT name, type, sql FROM sqlite_master " .
|
|
"WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' " .
|
|
"ORDER BY type, lower(name)");
|
|
# Preserve the named fields expected by object-list callers.
|
|
return [ map {
|
|
{ 'name' => $_->[0], 'type' => $_->[1], 'sql' => $_->[2] }
|
|
} @$rows ];
|
|
}
|
|
|
|
# sub sqlite_object(dbh, name)
|
|
# Returns a named table or view, or undef when it does not exist.
|
|
sub sqlite_object
|
|
{
|
|
my ($dbh, $name) = @_;
|
|
my $rows = &sqlite_select_metadata_rows($dbh,
|
|
"SELECT name, type, sql FROM sqlite_master " .
|
|
"WHERE type IN ('table', 'view') AND name = ? " .
|
|
"AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'", $name);
|
|
# Return undef when the named object is absent.
|
|
return undef if (!@$rows);
|
|
return { 'name' => $rows->[0]->[0], 'type' => $rows->[0]->[1],
|
|
'sql' => $rows->[0]->[2] };
|
|
}
|
|
|
|
# sub sqlite_table_columns(dbh, table)
|
|
# Returns visible and generated columns for a table or view.
|
|
sub sqlite_table_columns
|
|
{
|
|
my ($dbh, $table) = @_;
|
|
my $quoted = &sqlite_quote_identifier($table);
|
|
# Prefer table_xinfo because it includes generated columns.
|
|
my $rows = &sqlite_select_metadata_rows($dbh,
|
|
"PRAGMA table_xinfo($quoted)");
|
|
# Older SQLite versions treat an unknown table_xinfo pragma as an empty result.
|
|
$rows = &sqlite_select_metadata_rows($dbh, "PRAGMA table_info($quoted)")
|
|
if (!@$rows);
|
|
my @fields = ('cid', 'name', 'type', 'notnull', 'dflt_value', 'pk',
|
|
'hidden');
|
|
my @columns;
|
|
# Convert positional PRAGMA rows to the named fields used by callers.
|
|
foreach my $row (@$rows) {
|
|
my %column;
|
|
for(my $i = 0; $i < @$row && $i < @fields; $i++) {
|
|
$column{$fields[$i]} = $row->[$i];
|
|
}
|
|
# table_info results do not include the hidden-column flag.
|
|
$column{'hidden'} = 0 if (!defined($column{'hidden'}));
|
|
push(@columns, \%column);
|
|
}
|
|
return [ grep { $_->{'hidden'} != 1 } @columns ];
|
|
}
|
|
|
|
# sub sqlite_column_selects(&columns, [&blob-columns-to-keep], [shorten-text])
|
|
# Returns value, storage class and length expressions. BLOB bodies are omitted
|
|
# unless requested. Set shorten-text only for display queries that may safely
|
|
# return a bounded prefix instead of the full stored value.
|
|
sub sqlite_column_selects
|
|
{
|
|
my ($columns, $keep_blobs, $shorten_text) = @_;
|
|
$keep_blobs ||= { };
|
|
my (@values, @types, @lengths);
|
|
my $text_limit;
|
|
# Limit one row's text prefixes to half the result budget at four bytes per
|
|
# UTF-8 character. The other half remains for identities and row metadata.
|
|
if ($shorten_text) {
|
|
my $columns_count = scalar(@$columns) || 1;
|
|
my $safe_limit = int(&sqlite_result_bytes_limit() /
|
|
(8 * $columns_count));
|
|
# Always permit at least one character per displayed column.
|
|
$safe_limit = 1 if ($safe_limit < 1);
|
|
$text_limit = int($config{'max_text'} || 0);
|
|
# Use the safe share when the configured display limit is larger or absent.
|
|
$text_limit = $safe_limit if ($text_limit < 1 ||
|
|
$text_limit > $safe_limit);
|
|
}
|
|
foreach my $column (@$columns) {
|
|
# Fetch BLOB bytes only when they identify the row.
|
|
my $name = $column->{'name'};
|
|
my $quoted = &sqlite_quote_identifier($name);
|
|
my $text_case = $shorten_text ?
|
|
"WHEN typeof($quoted) = 'text' THEN substr($quoted, 1, ".
|
|
$text_limit.") " : '';
|
|
push(@values, $keep_blobs->{$name} ? $quoted :
|
|
"CASE WHEN typeof($quoted) = 'blob' THEN NULL ".
|
|
$text_case."ELSE $quoted END");
|
|
push(@types, "typeof($quoted)");
|
|
push(@lengths, "length($quoted)");
|
|
}
|
|
return (\@values, \@types, \@lengths);
|
|
}
|
|
|
|
# sub sqlite_identity(dbh, object, columns)
|
|
# Chooses a stable row identity for editing a table.
|
|
sub sqlite_identity
|
|
{
|
|
my ($dbh, $object, $columns) = @_;
|
|
# Views have no identity that is safe for row updates.
|
|
return undef if ($object->{'type'} ne 'table');
|
|
my %names = map { lc($_->{'name'}) => 1 } @$columns;
|
|
# Ordinary tables can use an unshadowed SQLite rowid alias.
|
|
if (($object->{'sql'} || '') !~ /\bWITHOUT\s+ROWID\b/i) {
|
|
foreach my $rowid ('rowid', '_rowid_', 'oid') {
|
|
# Use the first alias that is not shadowed by a declared column.
|
|
return { 'rowid' => $rowid, 'columns' => [ ] }
|
|
if (!$names{$rowid});
|
|
}
|
|
}
|
|
# Other tables need a declared primary key.
|
|
my @pk = sort { $a->{'pk'} <=> $b->{'pk'} }
|
|
grep { $_->{'pk'} } @$columns;
|
|
return @pk ? { 'columns' => [ map { $_->{'name'} } @pk ] } : undef;
|
|
}
|
|
|
|
# sub sqlite_encode_key(values...)
|
|
# Encodes one or more row identity values into an opaque URL-safe token.
|
|
sub sqlite_encode_key
|
|
{
|
|
my (@values) = @_;
|
|
my $raw = '';
|
|
foreach my $value (@values) {
|
|
# Preserve NULL separately from empty text.
|
|
if (!defined($value)) {
|
|
$raw .= 'N;';
|
|
}
|
|
else {
|
|
# Record the byte length and whether to restore the Unicode
|
|
# flag.
|
|
my $bytes = $value;
|
|
my $tag = utf8::is_utf8($bytes) ? 'U' : 'V';
|
|
# Encode Unicode before recording a byte length in the token.
|
|
utf8::encode($bytes) if ($tag eq 'U');
|
|
$raw .= $tag.length($bytes).':'.$bytes.';';
|
|
}
|
|
}
|
|
# Convert the framed values to unpadded URL-safe Base64.
|
|
my $encoded = MIME::Base64::encode_base64($raw, '');
|
|
$encoded =~ tr!+/!-_!;
|
|
$encoded =~ s/=+$//;
|
|
return $encoded;
|
|
}
|
|
|
|
# sub sqlite_decode_key(token, count)
|
|
# Decodes and validates a row identity token.
|
|
sub sqlite_decode_key
|
|
{
|
|
my ($token, $count) = @_;
|
|
# Normalize an omitted token so it follows the normal validation path.
|
|
$token = '' if (!defined($token));
|
|
$token =~ tr!-_!+/!;
|
|
$token .= '=' x ((4 - length($token) % 4) % 4);
|
|
my $raw = MIME::Base64::decode_base64($token);
|
|
my @values;
|
|
while (length($raw)) {
|
|
# Restore an encoded NULL field.
|
|
if ($raw =~ s/^N;//) {
|
|
push(@values, undef);
|
|
}
|
|
elsif ($raw =~ s/^([VU])([0-9]+)://) {
|
|
# Restore one length-prefixed byte or Unicode value.
|
|
my $tag = $1;
|
|
my $length = $2;
|
|
$length <= length($raw) || &error($text{'error_key'});
|
|
my $value = substr($raw, 0, $length, '');
|
|
$raw =~ s/^;// || &error($text{'error_key'});
|
|
# Restore Perl's Unicode flag only for values encoded with the
|
|
# U tag.
|
|
utf8::decode($value) if ($tag eq 'U');
|
|
push(@values, $value);
|
|
}
|
|
else {
|
|
# Reject malformed tags, lengths and separators.
|
|
&error($text{'error_key'});
|
|
}
|
|
}
|
|
@values == $count || &error($text{'error_key'});
|
|
return @values;
|
|
}
|
|
|
|
# sub sqlite_identity_where(&identity, &values)
|
|
# Builds a parameterized WHERE clause for a row identity.
|
|
sub sqlite_identity_where
|
|
{
|
|
my ($identity, $values) = @_;
|
|
# A rowid identity needs one direct equality comparison.
|
|
if ($identity->{'rowid'}) {
|
|
return ($identity->{'rowid'}.' = ?', @$values);
|
|
}
|
|
# Match every component of a declared composite primary key.
|
|
my @parts = map { &sqlite_quote_identifier($_).' IS ?' }
|
|
@{$identity->{'columns'}};
|
|
return (join(' AND ', @parts), @$values);
|
|
}
|
|
|
|
# sub sqlite_value_is_binary(value)
|
|
# Returns true when a database value cannot be safely shown as UTF-8 text.
|
|
sub sqlite_value_is_binary
|
|
{
|
|
my ($value) = @_;
|
|
# NULL has no bytes to classify.
|
|
return 0 if (!defined($value));
|
|
# Control bytes are unsafe even within valid UTF-8.
|
|
return 1 if ($value =~ /[\x00-\x08\x0b\x0c\x0e-\x1f]/);
|
|
# Perl has already validated values carrying its Unicode flag.
|
|
return 0 if (utf8::is_utf8($value));
|
|
return !eval { decode('UTF-8', $value, FB_CROAK); 1 };
|
|
}
|
|
|
|
# sub sqlite_display_value(value, [storage-type], [storage-length])
|
|
# Formats a database value for safe display in an HTML table. Optional SQLite
|
|
# storage metadata identifies BLOBs and preserves the original length when SQL
|
|
# returns only a text prefix.
|
|
sub sqlite_display_value
|
|
{
|
|
my ($value, $storage_type, $storage_length) = @_;
|
|
# Use SQLite's storage class so ASCII-only BLOBs remain binary.
|
|
if (defined($storage_type) && $storage_type eq 'blob') {
|
|
my $length = defined($storage_length) ? $storage_length :
|
|
length(defined($value) ? $value : '');
|
|
return &text('value_blob', $length);
|
|
}
|
|
# Render SQL NULL with a distinct visual marker.
|
|
return &ui_tag('i', $text{'value_null'}) if (!defined($value));
|
|
# Hide binary values when no SQLite storage class was supplied.
|
|
return &text('value_blob', length($value)) if (&sqlite_value_is_binary($value));
|
|
my $shown = $value;
|
|
my $max = int($config{'max_text'} || 0);
|
|
my $truncated;
|
|
# Table queries provide the original character count after shortening text
|
|
# inside SQLite, so the display still indicates omitted content.
|
|
my $original_length = defined($storage_type) && $storage_type eq 'text' &&
|
|
defined($storage_length) ? $storage_length : length($shown);
|
|
# Apply the display limit before building the details disclosure.
|
|
if ($max > 0 && length($shown) > $max) {
|
|
$shown = substr($shown, 0, $max);
|
|
$truncated = 1;
|
|
}
|
|
# Preserve the truncation marker when SQL already shortened the value.
|
|
$truncated = 1 if ($original_length > length($shown));
|
|
my $preview_length = 128;
|
|
# Short values need no expandable details control.
|
|
if (length($shown) <= $preview_length) {
|
|
return &ui_tag('span', &html_escape($shown).
|
|
($truncated ? $text{'value_more'} : ''), {
|
|
'style' => 'white-space:normal;overflow-wrap:anywhere;'.
|
|
'word-break:break-word;',
|
|
});
|
|
}
|
|
|
|
# Longer values show a preview and keep the rest in a disclosure whose
|
|
# summary counts the hidden characters.
|
|
my $preview = substr($shown, 0, $preview_length);
|
|
my $remainder = substr($shown, $preview_length);
|
|
my $remaining = $original_length - $preview_length;
|
|
my $summary = $truncated ?
|
|
&text('value_more_chars_truncated', length($remainder), $remaining) :
|
|
&text('value_more_chars', $remaining);
|
|
my $content = &html_escape($remainder).
|
|
($truncated ? $text{'value_more'} : '');
|
|
return &ui_tag('span', &html_escape($preview).' '.&ui_details({
|
|
'html' => 1,
|
|
'title' => &html_escape($summary),
|
|
'content' => &ui_tag('span', $content, {
|
|
'style' => 'white-space:normal;overflow-wrap:anywhere;',
|
|
}),
|
|
'class' => 'inline inlined',
|
|
}), {
|
|
'style' => 'white-space:normal;overflow-wrap:anywhere;'.
|
|
'word-break:break-word;vertical-align:top;',
|
|
});
|
|
}
|
|
|
|
# sub sqlite_install_authorizer(dbh)
|
|
# Prevents raw SQL from accessing files outside the open database.
|
|
sub sqlite_install_authorizer
|
|
{
|
|
my ($dbh) = @_;
|
|
eval { require DBD::SQLite::Constants; };
|
|
# Disable direct SQL entirely when the driver cannot enforce the sandbox.
|
|
if ($@ || !$dbh->can('sqlite_set_authorizer')) {
|
|
&error($text{'exec_noauthorizer'});
|
|
}
|
|
my $ok = DBD::SQLite::Constants::SQLITE_OK();
|
|
my $deny = DBD::SQLite::Constants::SQLITE_DENY();
|
|
my $attach = DBD::SQLite::Constants::SQLITE_ATTACH();
|
|
my $detach = DBD::SQLite::Constants::SQLITE_DETACH();
|
|
my $pragma = DBD::SQLite::Constants::SQLITE_PRAGMA();
|
|
my $function = DBD::SQLite::Constants::SQLITE_FUNCTION();
|
|
# Disable extension loading when the driver exposes the control method.
|
|
$dbh->sqlite_enable_load_extension(0)
|
|
if ($dbh->can('sqlite_enable_load_extension'));
|
|
# sub authorizer(action, argument1, argument2)
|
|
# Denies SQL operations that can reach files outside the approved database.
|
|
$dbh->sqlite_set_authorizer(sub {
|
|
my ($action, $arg1, $arg2) = @_;
|
|
# ATTACH and DETACH could bypass the module's database path ACL.
|
|
return $deny if ($action == $attach || $action == $detach);
|
|
# Directory-changing pragmas can redirect SQLite's temporary files.
|
|
return $deny if ($action == $pragma && defined($arg1) &&
|
|
$arg1 =~ /^(?:data|temp)_store_directory$/i);
|
|
# File and extension helpers could read or write arbitrary server paths.
|
|
return $deny if ($action == $function &&
|
|
grep { defined($_) && /^(?:load_extension|readfile|writefile)$/i }
|
|
($arg1, $arg2));
|
|
return $ok;
|
|
});
|
|
}
|
|
|
|
# sub sqlite_query(dbh, sql, [limit])
|
|
# Executes one SQL statement and returns its columns, rows, affected-row count
|
|
# and truncation reason. limit optionally caps returned rows and is clamped
|
|
# from 1 to 100,000.
|
|
sub sqlite_query
|
|
{
|
|
my ($dbh, $sql, $limit) = @_;
|
|
$limit = int($limit || 0);
|
|
# Use one row as the minimum result limit.
|
|
$limit = 1 if ($limit < 1);
|
|
# Cap retained results at 100,000 rows.
|
|
$limit = 100000 if ($limit > 100000);
|
|
# The driver prepares the first statement only, and reports what follows
|
|
# it just while multiple statements are allowed. Allow them for the
|
|
# prepare alone, which runs nothing, then refuse any remainder instead of
|
|
# dropping it silently.
|
|
$dbh->{'sqlite_allow_multiple_statements'} = 1;
|
|
my $sth = eval { $dbh->prepare($sql) };
|
|
my $prepare_error = $@;
|
|
$dbh->{'sqlite_allow_multiple_statements'} = 0;
|
|
die $prepare_error if ($prepare_error);
|
|
my $rest = $sth->{'sqlite_unprepared_statements'};
|
|
die $text{'exec_multiple'}."\n" if (defined($rest) && $rest =~ /\S/);
|
|
# Result columns are known before execution, so only statements that return
|
|
# rows use the value ceiling. Writes keep SQLite's normal value-size limit, so
|
|
# they can still save large values.
|
|
my @columns = $sth->{'NAME'} ? @{$sth->{'NAME'}} : ( );
|
|
# sub run()
|
|
# Executes the prepared statement and returns rows, stop reason and row count.
|
|
my $run = sub {
|
|
$sth->execute();
|
|
my ($rows, $stopped) = @columns ? &sqlite_fetch_rows($sth, $limit) :
|
|
([ ], undef);
|
|
my $affected = $sth->rows();
|
|
$sth->finish();
|
|
return ($rows, $stopped, $affected);
|
|
};
|
|
my ($rows, $stopped, $affected) = eval {
|
|
@columns ? &sqlite_with_value_limit($dbh, $run) : $run->();
|
|
};
|
|
# Release the statement before passing a failure on.
|
|
if ($@) {
|
|
my $error = $@;
|
|
eval { $sth->finish(); };
|
|
die $error;
|
|
}
|
|
return {
|
|
'columns' => \@columns,
|
|
'rows' => $rows,
|
|
'affected' => $affected,
|
|
'truncated' => $stopped ? 1 : 0,
|
|
'truncated_by' => $stopped,
|
|
};
|
|
}
|
|
|
|
# sub sqlite_column_types()
|
|
# Returns the column types offered by the table form, in display order.
|
|
sub sqlite_column_types
|
|
{
|
|
return ( 'INTEGER', 'TEXT', 'REAL', 'BLOB', 'NUMERIC' );
|
|
}
|
|
|
|
# sub sqlite_valid_name(name)
|
|
# Returns true for a usable table or column name: non-empty, free of
|
|
# control characters and not in SQLite's reserved sqlite_ namespace.
|
|
sub sqlite_valid_name
|
|
{
|
|
my ($name) = @_;
|
|
return 0 if (!defined($name) || $name eq '');
|
|
return 0 if ($name =~ /[\0-\x1f\x7f]/);
|
|
return 0 if ($name =~ /^sqlite_/i);
|
|
return 1;
|
|
}
|
|
|
|
# sub sqlite_default_literal(value)
|
|
# Returns the SQL literal for a column default typed into the form:
|
|
# numbers and the CURRENT_* keywords as they are, NULL as the keyword,
|
|
# and any other text as a quoted string.
|
|
sub sqlite_default_literal
|
|
{
|
|
my ($value) = @_;
|
|
return 'NULL' if (uc($value) eq 'NULL');
|
|
return uc($value) if ($value =~ /^CURRENT_(?:TIME|DATE|TIMESTAMP)$/i);
|
|
return $value if ($value =~ /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/);
|
|
(my $quoted = $value) =~ s/'/''/g;
|
|
return "'".$quoted."'";
|
|
}
|
|
|
|
# sub sqlite_create_table_sql(name, &columns)
|
|
# Builds a CREATE TABLE statement from the form's column definitions, or
|
|
# dies with a message when the definition is invalid. Each column is a
|
|
# hash with name, type, null (1 when NULL is allowed), key ('', 'pk',
|
|
# 'pkai' or 'unique') and default (empty for none).
|
|
sub sqlite_create_table_sql
|
|
{
|
|
my ($name, $columns) = @_;
|
|
&sqlite_valid_name($name) ||
|
|
die &text('create_ename', &html_escape($name))."\n";
|
|
@$columns || die $text{'create_ecolumns'}."\n";
|
|
my %types = map { $_ => 1 } &sqlite_column_types();
|
|
my (%seen, @pk, $autoincrement);
|
|
foreach my $column (@$columns) {
|
|
my $cname = $column->{'name'};
|
|
&sqlite_valid_name($cname) ||
|
|
die &text('create_ecolumn', &html_escape($cname))."\n";
|
|
# Column names are unique without regard to case.
|
|
!$seen{lc($cname)}++ ||
|
|
die &text('create_edup', &html_escape($cname))."\n";
|
|
$types{$column->{'type'}} ||
|
|
die &text('create_etype', &html_escape($cname))."\n";
|
|
push(@pk, $column) if ($column->{'key'} =~ /^pk(ai)?$/);
|
|
$autoincrement = $column if ($column->{'key'} eq 'pkai');
|
|
}
|
|
# AUTOINCREMENT needs a lone INTEGER PRIMARY KEY column.
|
|
if ($autoincrement && (@pk != 1 || $autoincrement->{'type'} ne 'INTEGER')) {
|
|
die &text('create_eauto', &html_escape($autoincrement->{'name'}))."\n";
|
|
}
|
|
my @defs;
|
|
foreach my $column (@$columns) {
|
|
my $def = &sqlite_quote_identifier($column->{'name'}).' '.$column->{'type'};
|
|
my $is_pk = $column->{'key'} =~ /^pk(ai)?$/;
|
|
# A single key column is declared on the column itself, so an INTEGER
|
|
# one becomes the rowid alias.
|
|
if ($is_pk && @pk == 1) {
|
|
$def .= ' PRIMARY KEY';
|
|
$def .= ' AUTOINCREMENT' if ($column->{'key'} eq 'pkai');
|
|
}
|
|
# Key columns never allow NULL, which SQLite would otherwise accept in
|
|
# non-INTEGER primary keys.
|
|
$def .= ' NOT NULL' if (!$column->{'null'} || $is_pk);
|
|
$def .= ' UNIQUE' if ($column->{'key'} eq 'unique');
|
|
$def .= ' DEFAULT '.&sqlite_default_literal($column->{'default'})
|
|
if (defined($column->{'default'}) && $column->{'default'} ne '');
|
|
push(@defs, $def);
|
|
}
|
|
# Several key columns form one table-level composite key.
|
|
push(@defs, 'PRIMARY KEY ('.join(', ',
|
|
map { &sqlite_quote_identifier($_->{'name'}) } @pk).')') if (@pk > 1);
|
|
return 'CREATE TABLE '.&sqlite_quote_identifier($name).
|
|
' ('.join(', ', @defs).')';
|
|
}
|
|
|
|
# sub sqlite_supports(feature, [version])
|
|
# Returns true when the SQLite library has a feature: rename_column (3.25 and
|
|
# later), drop_column (3.35 and later), or full-path nofollow (3.39 and later).
|
|
# Here nofollow means that every database path component is checked, not only
|
|
# the final filename. version defaults to the loaded library's version number.
|
|
sub sqlite_supports
|
|
{
|
|
my ($feature, $version) = @_;
|
|
my %needs = ( 'rename_column' => 3025000, 'drop_column' => 3035000,
|
|
'nofollow' => 3039000 );
|
|
# The driver sets the version once DBD::SQLite is loaded, so load it here
|
|
# when nothing has yet; otherwise a check made before the first connection
|
|
# would report every feature as missing.
|
|
if (!defined($version)) {
|
|
&sqlite_load_driver();
|
|
no warnings 'once';
|
|
$version = $DBD::SQLite::sqlite_version_number;
|
|
}
|
|
return $needs{$feature} && $version && $version >= $needs{$feature} ? 1 : 0;
|
|
}
|
|
|
|
# sub sqlite_add_column_sql(table, &column)
|
|
# Builds an ALTER TABLE ... ADD COLUMN statement, or dies when SQLite cannot
|
|
# add such a column: the name must be valid, the type one of those
|
|
# offered, a NOT NULL column needs a constant default to fill existing
|
|
# rows, and the default cannot be the current time or date.
|
|
sub sqlite_add_column_sql
|
|
{
|
|
my ($table, $column) = @_;
|
|
&sqlite_valid_name($column->{'name'}) ||
|
|
die &text('create_ecolumn', &html_escape($column->{'name'}))."\n";
|
|
my %types = map { $_ => 1 } &sqlite_column_types();
|
|
$types{$column->{'type'}} ||
|
|
die &text('create_etype', &html_escape($column->{'name'}))."\n";
|
|
my $default = defined($column->{'default'}) ? $column->{'default'} : '';
|
|
my $has_default = $default ne '' && uc($default) ne 'NULL';
|
|
# Existing rows take the default, so NOT NULL is impossible without one.
|
|
die $text{'column_enotnull'}."\n" if (!$column->{'null'} && !$has_default);
|
|
die $text{'column_ecurrent'}."\n"
|
|
if ($default =~ /^CURRENT_(?:TIME|DATE|TIMESTAMP)$/i);
|
|
my $sql = 'ALTER TABLE '.&sqlite_quote_identifier($table).' ADD COLUMN '.
|
|
&sqlite_quote_identifier($column->{'name'}).' '.$column->{'type'};
|
|
$sql .= ' NOT NULL' if (!$column->{'null'});
|
|
$sql .= ' DEFAULT '.&sqlite_default_literal($default) if ($default ne '');
|
|
return $sql;
|
|
}
|
|
|
|
# sub sqlite_table_form(path, &input, count)
|
|
# Returns the form for creating a table with the given number of column
|
|
# rows, refilled from the submitted values when the form is shown again.
|
|
sub sqlite_table_form
|
|
{
|
|
my ($path, $in, $count) = @_;
|
|
my $rv = &ui_form_start('create_table.cgi', 'post');
|
|
$rv .= &ui_hidden('db', $path);
|
|
$rv .= &ui_hidden('fields', $count);
|
|
$rv .= &ui_table_start($text{'create_header'}, 'width=100%', 2);
|
|
$rv .= &ui_table_row($text{'create_name'},
|
|
&ui_textbox('name', $in->{'name'}, 40));
|
|
|
|
# One row of inputs per column, with the first row set up as the usual
|
|
# autoincrementing integer key.
|
|
my @types = map { [ $_ ] } &sqlite_column_types();
|
|
my @keys = (
|
|
[ '', $text{'create_key_none'} ],
|
|
[ 'pk', $text{'create_key_pk'} ],
|
|
[ 'pkai', $text{'create_key_pkai'} ],
|
|
[ 'unique', $text{'create_key_unique'} ] );
|
|
my $table = &ui_columns_start([
|
|
$text{'create_column'}, $text{'create_type'}, $text{'create_null'},
|
|
$text{'create_key'}, $text{'create_default'} ], 100, 0,
|
|
[ 'nowrap', 'nowrap', 'nowrap', 'nowrap', 'nowrap' ], undef, 0,
|
|
'no-hover');
|
|
# Cells are centered vertically, so the short radio buttons line up with
|
|
# the taller text inputs and selects beside them in every theme.
|
|
my @cell = ( "nowrap style='vertical-align:middle'" ) x 5;
|
|
for(my $i = 0; $i < $count; $i++) {
|
|
my $fresh = !defined($in->{'name_'.$i});
|
|
$table .= &ui_columns_row([
|
|
&ui_textbox('name_'.$i, $in->{'name_'.$i}, 25),
|
|
&ui_select('type_'.$i, $fresh ? ($i ? 'TEXT' : 'INTEGER') :
|
|
$in->{'type_'.$i}, \@types),
|
|
&ui_yesno_radio('null_'.$i, $fresh ? ($i ? 1 : 0) :
|
|
($in->{'null_'.$i} ? 1 : 0)),
|
|
&ui_select('key_'.$i, $fresh ? ($i ? '' : 'pkai') :
|
|
$in->{'key_'.$i}, \@keys),
|
|
&ui_textbox('default_'.$i, $in->{'default_'.$i}, 15),
|
|
], \@cell);
|
|
}
|
|
$table .= &ui_columns_end();
|
|
$rv .= &ui_table_row(undef, $table, 2);
|
|
$rv .= &ui_table_end();
|
|
$rv .= &ui_form_end([ [ 'create', $text{'create_ok'} ],
|
|
[ 'more', $text{'create_more'} ] ]);
|
|
return $rv;
|
|
}
|
|
|
|
# sub sqlite_recent_limit()
|
|
# Returns how many recently opened databases to keep per user. Zero
|
|
# disables the list.
|
|
sub sqlite_recent_limit
|
|
{
|
|
my $limit = $config{'recent'};
|
|
# Fall back to three entries when the option is unset or not a number.
|
|
$limit = 3 if (!defined($limit) || $limit !~ /^\d+$/);
|
|
# Cap the list at fifty entries.
|
|
return $limit > 50 ? 50 : int($limit);
|
|
}
|
|
|
|
# sub sqlite_recent_file()
|
|
# Returns the file holding the current user's recent list, or undef when
|
|
# no user is logged in. Lists live under the module configuration
|
|
# directory, which only root can write; once the module runs as a Unix
|
|
# user they live in that user's home instead.
|
|
sub sqlite_recent_file
|
|
{
|
|
return undef if (!defined($remote_user) || $remote_user eq '');
|
|
my $dir = defined($sqlite_unix_home) && $sqlite_unix_home =~ m!^/! ?
|
|
$sqlite_unix_home.'/.webmin-sqlite' :
|
|
$module_config_directory.'/recent';
|
|
# Encoding the name keeps every user in one flat directory, with no
|
|
# path separators or dots that could escape it.
|
|
return $dir.'/'.&urlize($remote_user);
|
|
}
|
|
|
|
# sub sqlite_read_recent_lines(file)
|
|
# Returns the raw lines of a recent list file, newest first.
|
|
sub sqlite_read_recent_lines
|
|
{
|
|
my ($file) = @_;
|
|
return () if (!-r $file);
|
|
my $data = &read_file_contents($file);
|
|
return grep { /\t/ } split(/\r?\n/, defined($data) ? $data : '');
|
|
}
|
|
|
|
# sub sqlite_list_recent()
|
|
# Returns the current user's recently opened databases, newest first, as
|
|
# hashes with path, time, tables and size. Entries whose file is gone or
|
|
# that the current ACL no longer allows are left out.
|
|
sub sqlite_list_recent
|
|
{
|
|
my $file = &sqlite_recent_file();
|
|
my $limit = &sqlite_recent_limit();
|
|
return () if (!$file || !$limit);
|
|
my @recent;
|
|
foreach my $line (&sqlite_read_recent_lines($file)) {
|
|
# The path comes last, so a tab inside it survives the split.
|
|
my ($time, $tables, $path) = split(/\t/, $line, 3);
|
|
next if (!defined($path) || $path !~ m!^/!);
|
|
# Check the ACL before touching the file, as the path validator does.
|
|
next if (!&sqlite_path_allowed($path));
|
|
my @st = stat($path);
|
|
# Skip deleted files and anything that is no longer a regular file.
|
|
next if (!@st || !-f _);
|
|
# Tolerate hand-edited or truncated fields without warnings.
|
|
push(@recent, {
|
|
'path' => $path,
|
|
'time' => $time =~ /^\d+$/ ? int($time) : 0,
|
|
'tables' => $tables =~ /^\d+$/ ? int($tables) : undef,
|
|
'size' => $st[7],
|
|
});
|
|
# Stop once the configured number of visible entries is reached.
|
|
last if (@recent >= $limit);
|
|
}
|
|
return @recent;
|
|
}
|
|
|
|
# sub sqlite_remember_recent(path, [tables])
|
|
# Moves a database to the top of the current user's recent list, keeping
|
|
# only the configured number of entries.
|
|
sub sqlite_remember_recent
|
|
{
|
|
my ($path, $tables) = @_;
|
|
my $file = &sqlite_recent_file();
|
|
my $limit = &sqlite_recent_limit();
|
|
return if (!$file || !$limit);
|
|
my ($dir) = $file =~ m!^(.*)/[^/]+$!;
|
|
# Create the directory on first use, accessible only to the process user.
|
|
if (!-d $dir) {
|
|
mkdir($dir, 0700) || return;
|
|
}
|
|
&lock_file($file);
|
|
# Drop the previous entry for this path, then put the new one first.
|
|
my @keep = grep { my @parts = split(/\t/, $_, 3);
|
|
!defined($parts[2]) || $parts[2] ne $path }
|
|
&sqlite_read_recent_lines($file);
|
|
unshift(@keep, time()."\t".int($tables || 0)."\t".$path);
|
|
splice(@keep, $limit) if (@keep > $limit);
|
|
&write_file_contents($file, join("\n", @keep)."\n");
|
|
chmod(0600, $file);
|
|
&unlock_file($file);
|
|
}
|
|
|
|
# sub sqlite_forget_recent([path])
|
|
# Removes one database from the current user's recent list, or the whole
|
|
# list when no path is given.
|
|
sub sqlite_forget_recent
|
|
{
|
|
my ($path) = @_;
|
|
my $file = &sqlite_recent_file();
|
|
return if (!$file || !-e $file);
|
|
&lock_file($file);
|
|
my @keep = defined($path) ?
|
|
grep { my @parts = split(/\t/, $_, 3);
|
|
!defined($parts[2]) || $parts[2] ne $path }
|
|
&sqlite_read_recent_lines($file) : ( );
|
|
# Write the remaining entries back, or drop the file with its last one.
|
|
if (@keep) {
|
|
&write_file_contents($file, join("\n", @keep)."\n");
|
|
chmod(0600, $file);
|
|
}
|
|
else {
|
|
# Nothing is left to list.
|
|
&unlink_file($file);
|
|
}
|
|
&unlock_file($file);
|
|
}
|
|
|
|
# sub sqlite_recent_time(time)
|
|
# Formats a last-opened time as today or yesterday with the clock time,
|
|
# or as a plain date for older entries.
|
|
sub sqlite_recent_time
|
|
{
|
|
my ($time) = @_;
|
|
return '' if (!$time);
|
|
my @then = localtime($time);
|
|
my @today = localtime(time());
|
|
my @yesterday = localtime(time() - 86400);
|
|
my $clock = sprintf('%02d:%02d', $then[2], $then[1]);
|
|
# Same calendar day as now.
|
|
if ($then[5] == $today[5] && $then[7] == $today[7]) {
|
|
return &text('index_today', $clock);
|
|
}
|
|
# The calendar day before now.
|
|
elsif ($then[5] == $yesterday[5] && $then[7] == $yesterday[7]) {
|
|
return &text('index_yesterday', $clock);
|
|
}
|
|
# Older entries show the date only, in the user's date format.
|
|
return &make_date($time, 1);
|
|
}
|
|
|
|
1;
|