Merge branch 'master' of github.com:zoneminder/ZoneMinder

This commit is contained in:
Isaac Connor 2020-06-05 13:04:22 -04:00
commit 30f90dbae4
26 changed files with 508 additions and 391 deletions

View File

@ -445,6 +445,10 @@ CREATE TABLE `Monitors` (
`Enabled` tinyint(3) unsigned NOT NULL default '1',
`LinkedMonitors` varchar(255),
`Triggers` set('X10') NOT NULL default '',
`ONVIF_URL` VARCHAR(255) NOT NULL DEFAULT '',
`ONVIF_Username` VARCHAR(64) NOT NULL DEFAULT '',
`ONVIF_Password` VARCHAR(64) NOT NULL DEFAULT '',
`ONVIF_Options` VARCHAR(64) NOT NULL DEFAULT '',
`Device` tinytext NOT NULL default '',
`Channel` tinyint(3) unsigned NOT NULL default '0',
`Format` int(10) unsigned NOT NULL default '0',

View File

@ -116,7 +116,7 @@ sub getParam {
} elsif ( defined($default) ) {
return $default;
}
Fatal("Missing mandatory parameter '$name'");
Error("Missing mandatory parameter '$name'");
}
sub executeCommand {

View File

@ -210,7 +210,7 @@ sub initialise( @ ) {
if ( my $logFile = $this->getTargettedEnv('LOG_FILE') ) {
$tempLogFile = $logFile;
}
($tempLogFile) = $tempLogFile =~ /^([\w\.\/]+)$/;
($tempLogFile) = $tempLogFile =~ /^([_\-\w\.\/]+)$/;
my $tempLevel = INFO;
my $tempTermLevel = $this->{termLevel};
@ -456,9 +456,9 @@ sub fileLevel {
if ( defined($fileLevel) ) {
$fileLevel = $this->limit($fileLevel);
# The filename might have changed, so always close and re-open
$this->closeFile() if ( $this->{fileLevel} > NOLOG );
$this->closeFile() if $this->{fileLevel} > NOLOG;
$this->{fileLevel} = $fileLevel;
$this->openFile() if ( $this->{fileLevel} > NOLOG );
$this->openFile() if $this->{fileLevel} > NOLOG;
}
return $this->{fileLevel};
}
@ -499,6 +499,7 @@ sub logFile {
sub openFile {
my $this = shift;
if ( open($LOGFILE, '>>', $this->{logFile}) ) {
$LOGFILE->autoflush() if $this->{autoFlush};
@ -519,7 +520,6 @@ sub openFile {
}
sub closeFile {
#my $this = shift;
close($LOGFILE) if fileno($LOGFILE);
}

View File

@ -39,12 +39,10 @@ our %EXPORT_TAGS = (
);
push( @{$EXPORT_TAGS{all}}, @{$EXPORT_TAGS{$_}} ) foreach keys %EXPORT_TAGS;
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } );
our @EXPORT = qw();
our $VERSION = $ZoneMinder::Base::VERSION;
use Data::UUID;
use vars qw( $verbose $soap_version );
@ -60,6 +58,9 @@ require WSDiscovery::TransportUDP;
sub deserialize_message {
my ($wsdl_client, $response) = @_;
if ( ! $response ) {
return;
}
# copied and adapted from SOAP::WSDL::Client
@ -74,20 +75,16 @@ sub deserialize_message {
}
# set class resolver if serializer supports it
$deserializer->set_class_resolver( $wsdl_client->get_class_resolver() )
if ( $deserializer->can('set_class_resolver') );
if $deserializer->can('set_class_resolver');
# Try deserializing response - there may be some,
# even if transport did not succeed (got a 500 response)
if ( ! $response ) {
return;
}
# as our faults are false, returning a success marker is the only
# reliable way of determining whether the deserializer succeeded.
# Custom deserializers may return an empty list, or undef,
# and $@ is not guaranteed to be undefined.
my ($success, $result_body, $result_header) = eval {
(1, $deserializer->deserialize( $response ));
(1, $deserializer->deserialize($response));
};
if ( defined $success ) {
return wantarray
@ -110,10 +107,7 @@ sub interpret_messages {
my @results;
foreach my $response ( @responses ) {
if ( $verbose ) {
print "Received message:\n" . $response . "\n";
}
print "Received message:\n" . $response . "\n" if $verbose;
my $result = deserialize_message($svc_discover, $response);
if ( not $result ) {
@ -151,15 +145,14 @@ sub interpret_messages {
foreach my $scope (split ' ', $scopes) {
if ( $scope =~ m|onvif://www\.onvif\.org/(.+)/(.*)| ) {
my ($attr, $value) = ($1,$2);
if ( 0 < $count ++) {
print ', ';
}
print ', ' if 0 < $count ++;
print $attr . '=\'' . $value . '\'';
$scopes{$attr} = $value;
}
}
print ")\n";
push @results, { xaddr=>$xaddr,
push @results, {
xaddr => $xaddr,
soap_version => $svc_discover->get_soap_version(),
scopes => \%scopes,
};
@ -170,7 +163,7 @@ sub interpret_messages {
# functions
sub discover {
my ( $soap_version, $net_interface ) = @_;
my ($soap_versions, $net_interface) = @_;
my @results;
## collect all responses
@ -185,7 +178,7 @@ sub discover {
my $uuid_gen = Data::UUID->new();
foreach my $version ( $soap_version ? ( $soap_version ) : ( '1.1', '1.2') ) {
foreach my $version ( $soap_versions ? ( split(',',$soap_versions) ) : ( '1.1', '1.2') ) {
my %services;
print "Probing for SOAP $version\n" if $verbose;
@ -195,7 +188,7 @@ sub discover {
$svc_discover->set_soap_version($version);
if ( $net_interface ) {
my $transport = $svc_discover->get_transport();
print "Setting net interface for $transport to $net_interface\n";
print "Setting net interface for $transport to $net_interface\n" if $verbose;
$transport->set_net_interface($net_interface);
}
@ -203,7 +196,15 @@ sub discover {
my $result = $svc_discover->ProbeOp(
{ # WSDiscovery::Types::ProbeType
(
($version eq '1.1') ?
(
Types => 'http://www.onvif.org/ver10/network/wsdl:NetworkVideoTransmitter http://www.onvif.org/ver10/device/wsdl:Device', # QNameListType
) : (
xmlattr => { 'xmlns:dn' => 'http://www.onvif.org/ver10/network/wsdl', },
)
),
Types => 'dn:NetworkVideoTransmitter', # QNameListType
Scopes => { value => '' },
},
WSDiscovery10::Elements::Header->new({
@ -215,22 +216,23 @@ sub discover {
print $result."\n" if $verbose;
push @results, interpret_messages($svc_discover, \%services, @responses);
@responses = ();
} # end foreach version
return @results;
} # end sub discover
sub profiles {
my ( $client ) = @_;
my ($client) = @_;
my $media = $client->get_endpoint('media');
if ( ! $media ) {
if ( !$media ) {
print "No media endpoint for client.\n";
return;
}
my $result = $media->GetProfiles( { } ,, );
if ( ! $result ) {
if ( !$result ) {
print "No result from GetProfiles.\n";
return;
}
@ -244,7 +246,7 @@ sub profiles {
print "No profiles returned from get_Profiles\n";
return;
}
print "Number of profiles found: " .(scalar @Profiles)."\n" if $verbose;
print 'Number of profiles found: ' .(scalar @Profiles)."\n" if $verbose;
my @profiles;
foreach my $profile ( @Profiles ) {
@ -339,12 +341,12 @@ sub move {
} # end sub move
sub metadata {
my ( $client ) = @_;
my ($client) = @_;
my $media = $client->get_endpoint('media');
die 'No media endpoint.' if !$media;
my $result = $media->GetMetadataConfigurations( { } ,, );
if ( ! $result ) {
if ( !$result ) {
print "No MetaDataConfigurations\n" if $verbose;
} else {
print $result . "\n";
@ -363,8 +365,6 @@ sub metadata {
}
1;
__END__

View File

@ -41,7 +41,7 @@ my $OPTIONS = 'v';
sub HELP_MESSAGE {
my ($fh, $pkg, $ver, $opts) = @_;
print $fh "Usage: " . __FILE__ . " [-v] probe <soap version> <network interface>\n";
print $fh "Usage: " . __FILE__ . " [-v] probe <soap versions> <network interface>\n";
print $fh " " . __FILE__ . " [-v] <command> <device URI> <soap version> <user> <password>\n";
print $fh <<EOF
Commands are:
@ -53,7 +53,7 @@ sub HELP_MESSAGE {
-v - increase verbosity
Device access parameters (for all commands but 'probe'):
device URL - the ONVIF Device service URL
soap version - SOAP version (1.1 or 1.2)
soap versions - SOAP versions (1.1 or 1.2 or 1.1,1.2)
user - username of a user with access to the device
password - password for the user
EOF
@ -69,7 +69,7 @@ if ( !getopts($OPTIONS) ) {
my $action = shift;
if ( ! defined $action ) {
if ( !defined $action ) {
HELP_MESSAGE(\*STDOUT);
exit(1);
}

View File

@ -121,6 +121,8 @@ GetOptions(
my $dbh = zmDbConnect(undef, { mysql_multi_statements=>1 } );
$Config{ZM_DB_USER} = $dbUser;
$Config{ZM_DB_PASS} = $dbPass;
# we escape dbpass with single quotes so that $ in the password has no effect, but dbpass could have a ' in it.
$dbPass =~ s/'/\\'/g;
if ( ! ($check || $freshen || $rename || $zoneFix || $migrateEvents || $version) ) {
if ( $Config{ZM_DYN_DB_VERSION} ) {
@ -384,21 +386,22 @@ if ( $version ) {
my $command = 'mysqldump';
if ( defined($portOrSocket) ) {
if ( $portOrSocket =~ /^\// ) {
$command .= " -S".$portOrSocket;
$command .= ' -S'.$portOrSocket;
} else {
$command .= " -h".$host." -P".$portOrSocket;
$command .= ' -h'.$host.' -P'.$portOrSocket;
}
} else {
$command .= " -h".$host;
$command .= ' -h'.$host;
}
if ( $dbUser ) {
$command .= ' -u'.$dbUser;
$command .= ' -p"'.$dbPass.'"' if $dbPass;
$command .= ' -p\''.$dbPass.'\'' if $dbPass;
}
my $backup = "@ZM_TMPDIR@/".$Config{ZM_DB_NAME}."-".$version.".dump";
$command .= " --add-drop-table --databases ".$Config{ZM_DB_NAME}." > ".$backup;
print( "Creating backup to $backup. This may take several minutes.\n" );
print( "Executing '$command'\n" ) if ( logDebugging() );
my $backup = '@ZM_TMPDIR@/'.$Config{ZM_DB_NAME}.'-'.$version.'.dump';
$command .= ' --add-drop-table --databases '.$Config{ZM_DB_NAME}.' > '.$backup;
print("Creating backup to $backup. This may take several minutes.\n");
print("Executing '$command'\n") if logDebugging();
($command) = $command =~ /(.*)/; # detaint
my $output = qx($command);
my $status = $? >> 8;
if ( $status || logDebugging() ) {
@ -982,7 +985,7 @@ sub patchDB {
}
if ( $dbUser ) {
$command .= ' -u'.$dbUser;
$command .= ' -p"'.$dbPass.'"' if $dbPass;
$command .= ' -p\''.$dbPass.'\'' if $dbPass;
}
$command .= ' '.$Config{ZM_DB_NAME}.' < ';
if ( $updateDir ) {
@ -993,6 +996,7 @@ sub patchDB {
$command .= '/zm_update-'.$version.'.sql';
print("Executing '$command'\n") if logDebugging();
($command) = $command =~ /(.*)/; # detaint
my $output = qx($command);
my $status = $? >> 8;
if ( $status || logDebugging() ) {

View File

@ -81,10 +81,11 @@ bool zmDbConnect() {
void zmDbClose() {
if ( zmDbConnected ) {
db_mutex.lock();
mysql_close( &dbconn );
mysql_close(&dbconn);
// mysql_init() call implicitly mysql_library_init() but
// mysql_close() does not call mysql_library_end()
mysql_library_end();
// We get segfaults and a hang when we call this. So just don't.
//mysql_library_end();
zmDbConnected = false;
db_mutex.unlock();
}
@ -96,6 +97,12 @@ MYSQL_RES * zmDbFetch(const char * query) {
return NULL;
}
db_mutex.lock();
// Might have been disconnected while we waited for the lock
if ( !zmDbConnected ) {
db_mutex.unlock();
Error("Not connected.");
return NULL;
}
if ( mysql_query(&dbconn, query) ) {
db_mutex.unlock();

View File

@ -714,11 +714,9 @@ bool EventStream::sendFrame(int delta_us) {
Image *image = NULL;
if ( filepath[0] ) {
Debug(1, "Loading image");
image = new Image(filepath);
} else if ( ffmpeg_input ) {
// Get the frame from the mp4 input
Debug(1,"Getting frame from ffmpeg");
FrameData *frame_data = &event_data->frames[curr_frame_id-1];
AVFrame *frame = ffmpeg_input->get_frame(
ffmpeg_input->get_video_stream_id(),
@ -767,7 +765,11 @@ Debug(1, "Loading image");
switch ( type ) {
case STREAM_JPEG :
send_image->EncodeJpeg(img_buffer, &img_buffer_size);
if ( send_image->EncodeJpeg(img_buffer, &img_buffer_size) ) {
Debug(1, "encoded JPEG");
} else {
// Failed
}
break;
case STREAM_ZIP :
#if HAVE_ZLIB_H
@ -787,10 +789,6 @@ Debug(1, "Loading image");
Fatal("Unexpected frame type %d", type);
break;
}
if ( send_image != image ) {
delete send_image;
send_image = NULL;
}
delete image;
image = NULL;
} // end if send_raw or not
@ -815,7 +813,7 @@ Debug(1, "Loading image");
fprintf(stdout, "Content-Length: %d\r\n\r\n", (int)filestat.st_size);
if ( zm_sendfile(fileno(stdout), fileno(fdj), 0, (int)filestat.st_size) != (int)filestat.st_size ) {
/* sendfile() failed, use standard way instead */
img_buffer_size = fread( img_buffer, 1, sizeof(temp_img_buffer), fdj );
img_buffer_size = fread(img_buffer, 1, sizeof(temp_img_buffer), fdj);
if ( fwrite(img_buffer, img_buffer_size, 1, stdout) != 1 ) {
fclose(fdj); /* Close the file handle */
Error("Unable to send raw frame %u: %s", curr_frame_id, strerror(errno));

View File

@ -975,10 +975,12 @@ int FfmpegCamera::CaptureAndRecord(
Error("Error count over 100, going to close and re-open stream");
return -1;
}
#if HAVE_LIBAVUTIL_HWCONTEXT_H
if ( (ret == AVERROR_INVALIDDATA ) && (hw_pix_fmt != AV_PIX_FMT_NONE) ) {
use_hwaccel = false;
return -1;
}
#endif
}
zm_av_packet_unref(&packet);
continue;

View File

@ -1905,11 +1905,11 @@ void Image::MaskPrivacy( const unsigned char *p_bitmask, const Rgb pixel_colour
/* RGB32 compatible: complete */
void Image::Annotate( const char *p_text, const Coord &coord, const unsigned int size, const Rgb fg_colour, const Rgb bg_colour )
{
strncpy( text, p_text, sizeof(text)-1 );
strncpy(text, p_text, sizeof(text)-1);
unsigned int index = 0;
unsigned int line_no = 0;
unsigned int text_len = strlen( text );
unsigned int text_len = strlen(text);
unsigned int line_len = 0;
const char *line = text;
@ -1928,10 +1928,10 @@ void Image::Annotate( const char *p_text, const Coord &coord, const unsigned int
const bool bg_trans = (bg_colour == RGB_TRANSPARENT);
int zm_text_bitmask = 0x80;
if (size == 2)
if ( size == 2 )
zm_text_bitmask = 0x8000;
while ( (index < text_len) && (line_len = strcspn( line, "\n" )) ) {
while ( (index < text_len) && (line_len = strcspn(line, "\n")) ) {
unsigned int line_width = line_len * ZM_CHAR_WIDTH * size;
@ -1967,10 +1967,19 @@ void Image::Annotate( const char *p_text, const Coord &coord, const unsigned int
unsigned char *temp_ptr = ptr;
for ( unsigned int x = lo_line_x, c = 0; x < hi_line_x && c < line_len; c++ ) {
int f;
if (size == 2)
if ( size == 2 ) {
if ( (line[c] * ZM_CHAR_HEIGHT * size) + r > sizeof(bigfontdata) ) {
Warning("Unsupported character %c in %s", line[c], line);
continue;
}
f = bigfontdata[(line[c] * ZM_CHAR_HEIGHT * size) + r];
else
} else {
if ( (line[c] * ZM_CHAR_HEIGHT) + r > sizeof(fontdata) ) {
Warning("Unsupported character %c in %s", line[c], line);
continue;
}
f = fontdata[(line[c] * ZM_CHAR_HEIGHT) + r];
}
for ( unsigned int i = 0; i < (ZM_CHAR_WIDTH * size) && x < hi_line_x; i++, x++, temp_ptr++ ) {
if ( f & (zm_text_bitmask >> i) ) {
if ( !fg_trans )
@ -1989,10 +1998,19 @@ void Image::Annotate( const char *p_text, const Coord &coord, const unsigned int
unsigned char *temp_ptr = ptr;
for ( unsigned int x = lo_line_x, c = 0; x < hi_line_x && c < line_len; c++ ) {
int f;
if (size == 2)
if ( size == 2 ) {
if ( (line[c] * ZM_CHAR_HEIGHT * size) + r > sizeof(bigfontdata) ) {
Warning("Unsupported character %c in %s", line[c], line);
continue;
}
f = bigfontdata[(line[c] * ZM_CHAR_HEIGHT * size) + r];
else
} else {
if ( (line[c] * ZM_CHAR_HEIGHT) + r > sizeof(fontdata) ) {
Warning("Unsupported character %c in %s", line[c], line);
continue;
}
f = fontdata[(line[c] * ZM_CHAR_HEIGHT) + r];
}
for ( unsigned int i = 0; i < (ZM_CHAR_WIDTH * size) && x < hi_line_x; i++, x++, temp_ptr += colours ) {
if ( f & (zm_text_bitmask >> i) ) {
if ( !fg_trans ) {
@ -2016,10 +2034,19 @@ void Image::Annotate( const char *p_text, const Coord &coord, const unsigned int
Rgb* temp_ptr = (Rgb*)ptr;
for ( unsigned int x = lo_line_x, c = 0; x < hi_line_x && c < line_len; c++ ) {
int f;
if (size == 2)
if ( size == 2 ) {
if ( (line[c] * ZM_CHAR_HEIGHT * size) + r > sizeof(bigfontdata) ) {
Warning("Unsupported character %c in %s", line[c], line);
continue;
}
f = bigfontdata[(line[c] * ZM_CHAR_HEIGHT * size) + r];
else
} else {
if ( (line[c] * ZM_CHAR_HEIGHT) + r > sizeof(fontdata) ) {
Warning("Unsupported character %c in %s", line[c], line);
continue;
}
f = fontdata[(line[c] * ZM_CHAR_HEIGHT) + r];
}
for ( unsigned int i = 0; i < (ZM_CHAR_WIDTH * size) && x < hi_line_x; i++, x++, temp_ptr++ ) {
if ( f & (zm_text_bitmask >> i) ) {
if ( !fg_trans ) {

View File

@ -552,14 +552,18 @@ bool Monitor::connect() {
snprintf(mem_file, sizeof(mem_file), "%s/zm.mmap.%d", staticConfig.PATH_MAP.c_str(), id);
map_fd = open(mem_file, O_RDWR|O_CREAT, (mode_t)0600);
if ( map_fd < 0 ) {
Fatal("Can't open memory map file %s, probably not enough space free: %s", mem_file, strerror(errno));
Error("Can't open memory map file %s, probably not enough space free: %s", mem_file, strerror(errno));
return false;
} else {
Debug(3, "Success opening mmap file at (%s)", mem_file);
}
struct stat map_stat;
if ( fstat(map_fd, &map_stat) < 0 )
Fatal("Can't stat memory map file %s: %s, is the zmc process for this monitor running?", mem_file, strerror(errno));
if ( fstat(map_fd, &map_stat) < 0 ) {
Error("Can't stat memory map file %s: %s, is the zmc process for this monitor running?", mem_file, strerror(errno));
close(map_fd);
return false;
}
if ( map_stat.st_size != mem_size ) {
if ( purpose == CAPTURE ) {

View File

@ -196,12 +196,11 @@ bool ValidateAccess(User *user, int mon_id, int function) {
return allowed;
}
int exit_zmu(int exit_code) {
void exit_zmu(int exit_code) {
logTerm();
zmDbClose();
exit(exit_code);
return exit_code;
}
int main(int argc, char *argv[]) {
@ -477,12 +476,18 @@ int main(int argc, char *argv[]) {
if ( mon_id > 0 ) {
Monitor *monitor = Monitor::Load(mon_id, function&(ZMU_QUERY|ZMU_ZONES), Monitor::QUERY);
if ( monitor ) {
if ( !monitor ) {
Error("Unable to load monitor %d", mon_id);
exit_zmu(-1);
} // end if ! MONITOR
if ( verbose ) {
printf("Monitor %d(%s)\n", monitor->Id(), monitor->Name());
}
if ( !monitor->connect() ) {
Error("Can't connect to capture daemon: %d %s", monitor->Id(), monitor->Name());
delete monitor;
monitor = NULL;
exit_zmu(-1);
}
@ -585,8 +590,9 @@ int main(int argc, char *argv[]) {
usleep(1000);
}
printf("Alarmed event id: %" PRIu64 "\n", monitor->GetLastEventId());
} // end if ! MONITOR
}
} // end if ZMU_ALARM
if ( function & ZMU_NOALARM ) {
if ( verbose )
printf("Forcing alarm off\n");
@ -687,6 +693,7 @@ int main(int argc, char *argv[]) {
have_output = true;
}
}
if ( have_output ) {
printf("\n");
}
@ -694,11 +701,8 @@ int main(int argc, char *argv[]) {
Usage();
}
delete monitor;
} else {
Error("Invalid monitor id %d", mon_id);
exit_zmu(-1);
}
} else {
monitor = NULL;
} else { // non monitor functions
if ( function & ZMU_QUERY ) {
#if ZM_HAS_V4L
char vidString[0x10000] = "";
@ -770,8 +774,9 @@ int main(int argc, char *argv[]) {
} // end foreach row
mysql_free_result(result);
} // end if function && ZMU_LIST
}
} // end if monitor id or not
delete user;
return exit_zmu(0);
}
exit_zmu(0);
return 0;
} // end int main()

View File

@ -1,5 +1,5 @@
<?php
ini_set('display_errors','0');
ini_set('display_errors', '0');
if ( empty($_REQUEST['id']) && empty($_REQUEST['eids']) ) {
ajaxError('No event id(s) supplied');
@ -28,8 +28,8 @@ if ( canView('Events') ) {
$ok = true;
break;
case 'deleteVideo' :
unlink( $videoFiles[$_REQUEST['id']] );
unset( $videoFiles[$_REQUEST['id']] );
unlink($videoFiles[$_REQUEST['id']]);
unset($videoFiles[$_REQUEST['id']]);
ajaxResponse();
break;
case 'export' :
@ -110,7 +110,11 @@ if ( canView('Events') ) {
false#,#Compress
#$exportStructure
) ) {
ajaxResponse(array('exportFile'=>$exportFile,'exportFormat'=>$exportFormat, 'connkey'=>(isset($_REQUEST['connkey'])?$_REQUEST['connkey']:'')));
ajaxResponse(array(
'exportFile'=>$exportFile,
'exportFormat'=>$exportFormat,
'connkey'=>(isset($_REQUEST['connkey'])?$_REQUEST['connkey']:'')
));
} else {
ajaxError('Export Failed');
}
@ -145,7 +149,7 @@ if ( canEdit('Events') ) {
break;
case 'delete' :
$Event = new ZM\Event($_REQUEST['id']);
if ( ! $Event->Id() ) {
if ( !$Event->Id() ) {
ajaxResponse(array('refreshEvent'=>false, 'refreshParent'=>true, 'message'=> 'Event not found.'));
} else {
$Event->delete();

View File

@ -1499,15 +1499,15 @@ function getLoad() {
function getDiskPercent($path = ZM_DIR_EVENTS) {
$total = disk_total_space($path);
if ( $total === false ) {
Error('disk_total_space returned false. Verify the web account user has access to ' . $path);
ZM\Error('disk_total_space returned false. Verify the web account user has access to ' . $path);
return 0;
} elseif ( $total == 0 ) {
Error('disk_total_space indicates the following path has a filesystem size of zero bytes ' . $path);
ZM\Error('disk_total_space indicates the following path has a filesystem size of zero bytes ' . $path);
return 100;
}
$free = disk_free_space($path);
if ( $free === false ) {
Error('disk_free_space returned false. Verify the web account user has access to ' . $path);
ZM\Error('disk_free_space returned false. Verify the web account user has access to ' . $path);
}
$space = round((($total - $free) / $total) * 100);
return $space;
@ -2063,7 +2063,7 @@ function logState() {
if ( $count['Level'] <= ZM\Logger::PANIC )
$count['Level'] = ZM\Logger::FATAL;
if ( !($levelCount = $levelCounts[$count['Level']]) ) {
Error('Unexpected Log level '.$count['Level']);
ZM\Error('Unexpected Log level '.$count['Level']);
next;
}
if ( $levelCount[1] && $count['LevelCount'] >= $levelCount[1] ) {

View File

@ -423,13 +423,16 @@ if ( (!ZM_OPT_USE_AUTH) or $user ) {
if ( count($storage_areas) <= 4 )
echo implode(', ', array_map($func, $storage_areas));
$shm_percent = getDiskPercent(ZM_PATH_MAP);
$shm_total_space = disk_total_space(ZM_PATH_MAP);
$shm_used = $shm_total_space - disk_free_space(ZM_PATH_MAP);
$class = '';
if ( $shm_percent > 98 ) {
$class = 'error';
} else if ( $shm_percent > 90 ) {
$class = 'warning';
}
echo ' <span class="'.$class.'">'.ZM_PATH_MAP.': '.$shm_percent.'%</span>';
echo ' <span class="'.$class.'" title="' . human_filesize($shm_used).' of '.human_filesize($shm_total_space).'">'.ZM_PATH_MAP.': '.$shm_percent.'%</span>';
?></li>
</ul>
<?php if ( defined('ZM_WEB_CONSOLE_BANNER') and ZM_WEB_CONSOLE_BANNER != '' ) { ?>

View File

@ -205,6 +205,7 @@ getBodyTopHTML();
<?php
ob_start();
?>
<div class="table-responsive">
<table class="table table-striped table-hover table-condensed consoleTable">
<thead class="thead-highlight">
<tr>
@ -394,5 +395,6 @@ for( $monitor_i = 0; $monitor_i < count($displayMonitors); $monitor_i += 1 ) {
</tfoot>
</table>
</div>
</div>
</form>
<?php xhtmlFooter() ?>

View File

@ -285,7 +285,7 @@ function getCmdResponse( respObj, respText ) {
}
} // end if haev a new auth hash
streamCmdTimer = streamQuery.delay( streamTimeout ); //Timeout is refresh rate for progressBox and time display
streamCmdTimer = streamQuery.delay(streamTimeout); //Timeout is refresh rate for progressBox and time display
}
var streamReq = new Request.JSON( {

View File

@ -212,7 +212,8 @@ function Monitor(monitorData) {
* @param {*} element - the event data passed by onchange callback
*/
function selectLayout(element) {
layout = $j(element).val();
var ddm = $j('#zmMontageLayout');
layout = ddm.val();
if ( layout_id = parseInt(layout) ) {
layout = layouts[layout];
@ -231,6 +232,7 @@ function selectLayout(element) {
if ( layout.Positions['default'] ) {
styles = layout.Positions['default'];
for ( style in styles ) {
console.log("Applying " + style + ' ' + styles[style]);
monitor_frame.css(style, styles[style]);
}
} else {
@ -241,7 +243,6 @@ function selectLayout(element) {
styles = layout.Positions['mId'+monitor.id];
for ( style in styles ) {
monitor_frame.css(style, styles[style]);
console.log("Applying " + style + ' : ' + styles[style]);
}
} else {
console.log("No Monitor styles to apply");

View File

@ -33,3 +33,7 @@ function configureButtons(element) {
form.saveBtn.disabled = (form.probe.selectedIndex==0);
}
}
function changeInterface(element) {
gotoStep1(element);
}

View File

@ -36,14 +36,14 @@ function createEventHtml(zm_event, frame) {
return eventHtml;
}
function showEventDetail( eventHtml ) {
$('instruction').addClass( 'hidden' );
function showEventDetail(eventHtml) {
$('instruction').addClass('hidden');
$('eventData').empty();
$('eventData').adopt( eventHtml );
$('eventData').removeClass( 'hidden' );
$('eventData').adopt(eventHtml);
$('eventData').removeClass('hidden');
}
function eventDataResponse( respObj, respText ) {
function eventDataResponse(respObj, respText) {
var zm_event = respObj.event;
if ( !zm_event ) {
console.log('Null event');
@ -179,7 +179,10 @@ function loadEventImage( imagePath, eid, fid, width, height, fps, videoName, dur
eventData.addEvent('click', showEvent.pass());
}
function tlZoomBounds( minTime, maxTime ) {
function tlZoomBounds(event) {
var target = event.target;
var minTime = target.getAttribute('data-zoom-min-time');
var maxTime = target.getAttribute('data-zoom-max-time');
location.replace('?view='+currentView+filterQuery+'&minTime='+minTime+'&maxTime='+maxTime);
}
@ -194,14 +197,20 @@ function tlPanRight() {
location.replace('?view='+currentView+filterQuery+'&midTime='+maxTime+'&range='+range);
}
window.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll("div.event").forEach(function(el) {
window.addEventListener('DOMContentLoaded', function() {
// These look like the code in skin.js, but that code doesn't select for divs.
document.querySelectorAll('div.event').forEach(function(el) {
el.onclick = window[el.getAttribute('data-on-click-this')].bind(el, el);
el.onmouseover = window[el.getAttribute('data-on-mouseover-this')].bind(el, el);
});
document.querySelectorAll("div.activity").forEach(function(el) {
document.querySelectorAll('div.activity').forEach(function(el) {
el.onclick = window[el.getAttribute('data-on-click-this')].bind(el, el);
el.onmouseover = window[el.getAttribute('data-on-mouseover-this')].bind(el, el);
});
document.querySelectorAll('div.zoom').forEach(function(el) {
el.onclick = function(ev) {
window[el.getAttribute('data-on-click')](ev);
};
});
});

View File

@ -695,12 +695,11 @@ function getControlResponse(respObj, respText) {
function controlCmd(event) {
button = event.target;
control = button.getAttribute('value');
xtell = button.getAttribute('xtell');
ytell = button.getAttribute('ytell');
xtell = button.getAttribute('data-xtell');
ytell = button.getAttribute('data-ytell');
var locParms = '';
if ( event && (xtell || ytell) ) {
console.log(event);
var target = event.target;
var coords = $(target).getCoordinates();
@ -849,7 +848,7 @@ function initPage() {
if ( refreshApplet && appletRefreshTime ) {
appletRefresh.delay(appletRefreshTime*1000);
}
if ( scale == 'auto' ) changeScale();
if ( scale == '0' || scale == 'auto' ) changeScale();
if ( window.history.length == 1 ) {
$j('#closeControl').html('');
}

View File

@ -169,12 +169,6 @@ if ( !ZM_PCRE )
// Currently unsupported
unset($httpMethods['jpegTags']);
$configTypes = array(
'None' => translate('None'),
'ONVIF' => 'ONVIF',
'PSIA' => 'PSIA',
);
if ( ZM_HAS_V4L1 ) {
$v4l1DeviceFormats = array(
'PAL' => 0,
@ -327,31 +321,31 @@ $orientations = array(
);
$deinterlaceopts = array(
'Disabled' => 0x00000000,
'Four field motion adaptive - Soft' => 0x00001E04, /* 30 change */
'Four field motion adaptive - Medium' => 0x00001404, /* 20 change */
'Four field motion adaptive - Hard' => 0x00000A04, /* 10 change */
'Discard' => 0x00000001,
'Linear' => 0x00000002,
'Blend' => 0x00000003,
'Blend (25%)' => 0x00000205
);
0x00000000 => 'Disabled',
0x00001E04 => 'Four field motion adaptive - Soft', /* 30 change */
0x00001404 => 'Four field motion adaptive - Medium', /* 20 change */
0x00000A04 => 'Four field motion adaptive - Hard', /* 10 change */
0x00000001 => 'Discard',
0x00000002 => 'Linear',
0x00000003 => 'Blend',
0x00000205 => 'Blend (25%)',
);
$deinterlaceopts_v4l2 = array(
'Disabled' => 0x00000000,
'Four field motion adaptive - Soft' => 0x00001E04, /* 30 change */
'Four field motion adaptive - Medium' => 0x00001404, /* 20 change */
'Four field motion adaptive - Hard' => 0x00000A04, /* 10 change */
'Discard' => 0x00000001,
'Linear' => 0x00000002,
'Blend' => 0x00000003,
'Blend (25%)' => 0x00000205,
'V4L2: Capture top field only' => 0x02000000,
'V4L2: Capture bottom field only' => 0x03000000,
'V4L2: Alternate fields (Bob)' => 0x07000000,
'V4L2: Progressive' => 0x01000000,
'V4L2: Interlaced' => 0x04000000
);
0x00000000 => 'Disabled',
0x00001E04 => 'Four field motion adaptive - Soft', /* 30 change */
0x00001404 => 'Four field motion adaptive - Medium', /* 20 change */
0x00000A04 => 'Four field motion adaptive - Hard', /* 10 change */
0x00000001 => 'Discard',
0x00000002 => 'Linear',
0x00000003 => 'Blend',
0x00000205 => 'Blend (25%)',
0x02000000 => 'V4L2: Capture top field only',
0x03000000 => 'V4L2: Capture bottom field only',
0x07000000 => 'V4L2: Alternate fields (Bob)',
0x01000000 => 'V4L2: Progressive',
0x04000000 => 'V4L2: Interlaced',
);
$fastblendopts = array(
'No blending' => 0,
@ -419,7 +413,7 @@ if ( canEdit('Monitors') ) {
$tabs = array();
$tabs['general'] = translate('General');
$tabs['source'] = translate('Source');
$tabs["config"] = translate('MetaConfig');
$tabs['onvif'] = translate('ONVIF');
if ( $monitor->Type() != 'WebSite' ) {
$tabs['storage'] = translate('Storage');
$tabs['timestamp'] = translate('Timestamp');
@ -498,7 +492,7 @@ if ( ZM_HAS_V4L && ($tab != 'source' || $monitor->Type() != 'Local') ) {
if ( $tab != 'onvif' ) {
?>
<input type="hidden" name="newMonitor[ONVIF_URL]" value="<?php echo validHtmlStr($monitor->ONVIF_URL()) ?>"/>
<input type="hidden" name="newMonitor[ONVIF_Username]" value="<?php echo validHtmlStr($monitor->ONVIF_User()) ?>"/>
<input type="hidden" name="newMonitor[ONVIF_Username]" value="<?php echo validHtmlStr($monitor->ONVIF_Username()) ?>"/>
<input type="hidden" name="newMonitor[ONVIF_Password]" value="<?php echo validHtmlStr($monitor->ONVIF_Password()) ?>"/>
<input type="hidden" name="newMonitor[ONVIF_Options]" value="<?php echo validHtmlStr($monitor->ONVIF_Options()) ?>"/>
<?php

View File

@ -39,16 +39,18 @@ function probeV4L() {
}
$monitors = array();
foreach ( dbFetchAll("SELECT Id, Name, Device,Channel FROM Monitors WHERE Type = 'Local' ORDER BY Device, Channel" ) as $monitor )
foreach ( dbFetchAll("SELECT Id, Name, Device, Channel FROM Monitors WHERE Type = 'Local' ORDER BY Device, Channel" ) as $monitor )
$monitors[$monitor['Device'].':'.$monitor['Channel']] = $monitor;
$devices = array();
$preferredStandards = array('PAL', 'NTSC');
$preferredFormats = array('BGR3', 'RGB3', 'YUYV', 'UYVY', 'JPEG', 'MJPG', '422P', 'YU12', 'GREY');
foreach ( $output as $line ) {
if ( !preg_match('/^d:([^|]+).*S:([^|]*).*F:([^|]+).*I:(\d+)\|(.+)$/', $line, $deviceMatches) )
ZM\Fatal("Can't parse command output '$line'");
$standards = explode('/',$deviceMatches[2]);
if ( !preg_match('/^d:([^|]+).*S:([^|]*).*F:([^|]+).*I:(\d+)\|(.+)$/', $line, $deviceMatches) ) {
ZM\Error("Can't parse command output '$line'");
continue;
}
$standards = explode('/', $deviceMatches[2]);
$preferredStandard = false;
foreach ( $preferredStandards as $standard ) {
if ( in_array( $standard, $standards ) ) {
@ -56,7 +58,7 @@ function probeV4L() {
break;
}
}
$formats = explode('/',$deviceMatches[3]);
$formats = explode('/', $deviceMatches[3]);
$preferredFormat = false;
foreach ( $preferredFormats as $format ) {
if ( in_array($format, $formats) ) {
@ -73,8 +75,10 @@ function probeV4L() {
);
$inputs = array();
for ( $i = 0; $i < $deviceMatches[4]; $i++ ) {
if ( !preg_match('/i'.$i.':([^|]+)\|i'.$i.'T:([^|]+)\|/', $deviceMatches[5], $inputMatches) )
ZM\Fatal("Can't parse input '".$deviceMatches[5]."'");
if ( !preg_match('/i'.$i.':([^|]+)\|i'.$i.'T:([^|]+)\|/', $deviceMatches[5], $inputMatches) ) {
ZM\Error("Can't parse input '".$deviceMatches[5]."'");
continue;
}
if ( $inputMatches[2] == 'Camera' ) {
$input = array(
'index' => $i,
@ -101,7 +105,7 @@ function probeV4L() {
$inputMonitor['Colours'] = 1;
$inputMonitor['SignalCheckColour'] = '#000023';
}
$inputDesc = base64_encode(serialize($inputMonitor));
$inputDesc = base64_encode(json_encode($inputMonitor));
$inputString = $deviceMatches[1].', chan '.$i.($input['free']?(' - '.translate('Available')):(' ('.$monitors[$input['id']]['Name'].')'));
$inputs[] = $input;
$cameras[$inputDesc] = $inputString;
@ -288,7 +292,7 @@ function probeNetwork() {
if ( isset($macBases[$macRoot]) ) {
$macBase = $macBases[$macRoot];
$camera = call_user_func($macBase['probeFunc'], $ip);
$sourceDesc = base64_encode(serialize($camera['monitor']));
$sourceDesc = base64_encode(json_encode($camera['monitor']));
$sourceString = $camera['model'].' @ '.$host;
if ( isset($monitors[$ip]) ) {
$monitor = $monitors[$ip];
@ -330,7 +334,7 @@ xhtmlHeaders(__FILE__, translate('MonitorProbe') );
</p>
<p>
<label for="probe"><?php echo translate('DetectedCameras') ?></label>
<?php echo buildSelect('probe', $cameras, 'configureButtons(this)'); ?>
<?php echo htmlSelect('probe', $cameras, null, array('data-on-change-this'=>'configureButtons(this)')); ?>
</p>
<div id="contentButtons">
<button type="button" name="saveBtn" value="Save" data-on-click-this="submitCamera" disabled="disabled">

View File

@ -75,10 +75,8 @@ zm_session_start();
$layout_id = '';
if ( isset($_COOKIE['zmMontageLayout']) ) {
$layout_id = $_SESSION['zmMontageLayout'] = $_COOKIE['zmMontageLayout'];
ZM\Logger::Debug("Using layout $layout_id");
} elseif ( isset($_SESSION['zmMontageLayout']) ) {
$layout_id = $_SESSION['zmMontageLayout'];
ZM\Logger::Debug("Using layout $layout_id from session");
}
$options = array();
@ -88,7 +86,7 @@ if ( $layout_id and is_numeric($layout_id) and isset($layoutsById[$layout_id]) )
$Layout = $layoutsById[$layout_id];
$Positions = json_decode($Layout->Positions(), true);
} else {
ZM\Logger::Debug("Layout not found");
ZM\Logger::Debug('Layout not found');
}
if ( $Layout and ( $Layout->Name() != 'Freeform' ) ) {
// Use layout instead of other options
@ -185,7 +183,7 @@ if ( $showZones ) {
</span>
<span id="layoutControl">
<label for="layout"><?php echo translate('Layout') ?></label>
<?php echo htmlSelect('zmMontageLayout', $layoutsById, $layout_id, array('data-on-change-this'=>'selectLayout')); ?>
<?php echo htmlSelect('zmMontageLayout', $layoutsById, $layout_id, array('data-on-change'=>'selectLayout')); ?>
</span>
<input type="hidden" name="Positions"/>
<button type="button" id="EditLayout" data-on-click-this="edit_layout"><?php echo translate('EditLayout') ?></button>

View File

@ -29,16 +29,17 @@ $cameras[0] = translate('ChooseDetectedCamera');
$profiles = array();
$profiles[0] = translate('ChooseDetectedProfile');
function execONVIF( $cmd ) {
function execONVIF($cmd) {
$shell_command = escapeshellcmd(ZM_PATH_BIN . "/zmonvif-probe.pl $cmd");
exec( $shell_command, $output, $status );
exec($shell_command, $output, $status);
if ( $status ) {
$html_output = implode( '<br/>', $output );
ZM\Fatal( "Unable to probe network cameras, status is '$status'. Output was:<br/><br/>
$html_output<br/><br/>
Please the following command from a command line for more information:<br/><br/>$shell_command"
$html_output = implode('<br/>', $output);
ZM\Error("Unable to probe network cameras, status is '$status'. Output was:
$html_output
Please run the following command from a command line for more information:
$shell_command"
);
} else {
ZM\Logger::Debug('Results from probe: '.implode('<br/>', $output));
@ -49,7 +50,8 @@ function execONVIF( $cmd ) {
function probeCameras($localIp) {
$cameras = array();
if ( $lines = @execONVIF('probe') ) {
$lines = @execONVIF('probe 1.1,1.2'.(isset($_REQUEST['interface']) ? ' '.isset($_REQUEST['interface']) : '' ));
if ( $lines ) {
foreach ( $lines as $line ) {
$line = rtrim($line);
if ( preg_match('|^(.+),(.+),\s\((.*)\)$|', $line, $matches) ) {
@ -64,18 +66,21 @@ function probeCameras($localIp) {
'SOAP' => $soapversion,
'ConfigURL' => $device_ep,
'ConfigOptions' => 'SOAP' . $soapversion,
'Notes' => '',
),
);
foreach ( preg_split('|,\s*|', $matches[3]) as $attr_val ) {
if ( preg_match('|(.+)=\'(.*)\'|', $attr_val, $tokens) ) {
if ( $tokens[1] == 'hardware' ) {
$camera['model'] = $tokens[2];
} elseif ( $tokens[1] == 'name' ) {
} else if ( $tokens[1] == 'name' ) {
$camera['monitor']['Name'] = $tokens[2];
} elseif ( $tokens[1] == 'location' ) {
} else if ( $tokens[1] == 'type' ) {
} else if ( $tokens[1] == 'location' or $tokens[1] == 'location/city' or $tokens[1] == 'location/country' ) {
$camera['monitor']['Notes'] .= $tokens[1].'='.$tokens[2]."\n";
// $camera['location'] = $tokens[2];
} else {
ZM\Logger::Debug('Unknown token ' . $tokens[1]);
ZM\Logger::Debug('Unknown token '.$tokens[1].' = '.$tokens[2]);
}
}
} // end foreach token
@ -165,6 +170,48 @@ if ( !isset($_REQUEST['step']) || ($_REQUEST['step'] == '1') ) {
<p>
<?php echo translate('OnvifProbeIntro') ?>
</p>
<p><label for="interface"><?php echo translate('Interface') ?></label>
<?php
$interfaces = array();
$default_interface = null;
exec('ip link', $output, $status);
if ( $status ) {
$html_output = implode('<br/>', $output);
ZM\Error("Unable to list network interfaces, status is '$status'. Output was:<br/><br/>$html_output");
} else {
foreach ( $output as $line ) {
if ( preg_match('/^\d+: ([[:alnum:]]+):/', $line, $matches ) ) {
if ( $matches[1] != 'lo' ) {
$interfaces[$matches[1]] = $matches[1];
} else {
ZM\Logger::Debug("No match for $line");
}
}
}
}
$routes = array();
exec('ip route', $output, $status);
if ( $status ) {
$html_output = implode('<br/>', $output);
ZM\Error("Unable to list network interfaces, status is '$status'. Output was:<br/><br/>$html_output");
} else {
foreach ( $output as $line ) {
if ( preg_match('/^default via [.[:digit:]]+ dev ([[:alnum:]]+)/', $line, $matches) ) {
$default_interface = $matches[1];
} else if ( preg_match('/^([.\/[:digit:]]+) dev ([[:alnum:]]+)/', $line, $matches) ) {
$interfaces[$matches[2]] .= ' ' . $matches[1];
}
} # end foreach line of output
}
echo htmlSelect('interface', $interfaces,
(isset($_REQUEST['interface']) ? $_REQUEST['interface'] : $default_interface),
array('data-on-change-this'=>'changeInterface') );
?>
</p>
<div id="DetectedCameras">
<p>
<label for="probe"><?php echo translate('DetectedCameras') ?></label>
<?php echo htmlSelect('probe', $cameras, null, array('data-on-change-this'=>'configureButtons')); ?>
@ -180,6 +227,7 @@ if ( !isset($_REQUEST['step']) || ($_REQUEST['step'] == '1') ) {
<label for="Password"><?php echo translate('Password') ?></label>
<input type="password" name="Password" data-on-change-this="configureButtons"/>
</p>
</div>
<div id="contentButtons">
<button type="button" data-on-click="closeWindow"><?php echo translate('Cancel') ?></button>
<button type="button" name="nextBtn" data-on-click-this="gotoStep2" disabled="disabled"><?php echo translate('Next') ?></button>

View File

@ -154,7 +154,7 @@ if ( isset($_REQUEST['midTime']) )
if ( isset($_REQUEST['maxTime']) )
$maxTime = validHtmlStr($_REQUEST['maxTime']);
if ( isset($range) ) {
if ( isset($range) and validInt($range) ) {
$halfRange = (int)($range/2);
if ( isset($midTime) ) {
$midTimeT = strtotime($midTime);
@ -616,7 +616,7 @@ function drawXGrid( $chart, $scale, $labelClass, $tickClass, $gridClass, $zoomCl
$zoomMinTime = strftime( STRF_FMT_DATETIME_DB, (int)($chart['data']['x']['lo'] + ($lastTick * $chart['data']['x']['density'])) );
$zoomMaxTime = strftime( STRF_FMT_DATETIME_DB, (int)($chart['data']['x']['lo'] + ($i * $chart['data']['x']['density'])) );
?>
<div class="<?php echo $zoomClass ?>" style="left: <?php echo 100*($lastTick-1)/$chart['graph']['width'] ?>%; width: <?php echo round(100*($i-$lastTick)/$chart['graph']['width'],1) ?>%;" title="<?php echo translate('ZoomIn') ?>" onclick="tlZoomBounds( '<?php echo $zoomMinTime ?>', '<?php echo $zoomMaxTime ?>' )"></div>
<div class="<?php echo $zoomClass ?>" style="left: <?php echo 100*($lastTick-1)/$chart['graph']['width'] ?>%; width: <?php echo round(100*($i-$lastTick)/$chart['graph']['width'],1) ?>%;" title="<?php echo translate('ZoomIn') ?>" data-on-click="tlZoomBounds" data-zoom-min-time="<?php echo $zoomMinTime ?>" data-zoom-max-time="<?php echo $zoomMaxTime ?>"></div>
<?php
}
$lastTick = $i;
@ -629,7 +629,7 @@ function drawXGrid( $chart, $scale, $labelClass, $tickClass, $gridClass, $zoomCl
$zoomMinTime = strftime( STRF_FMT_DATETIME_DB, (int)($chart['data']['x']['lo'] + ($lastTick * $chart['data']['x']['density'])) );
$zoomMaxTime = strftime( STRF_FMT_DATETIME_DB, (int)($chart['data']['x']['lo'] + ($i * $chart['data']['x']['density'])) );
?>
<div class="<?php echo $zoomClass ?>" style="left: <?php echo $lastTick-1 ?>px; width: <?php echo $i-$lastTick ?>px;" title="<?php echo translate('ZoomIn') ?>" onclick="tlZoomBounds( '<?php echo $zoomMinTime ?>', '<?php echo $zoomMaxTime ?>' )"></div>
<div class="<?php echo $zoomClass ?>" style="left: <?php echo $lastTick-1 ?>px; width: <?php echo $i-$lastTick ?>px;" title="<?php echo translate('ZoomIn') ?>" data-on-click="tlZoomBounds" data-zoom-min-time="<?php echo $zoomMinTime ?>" data-zoom-max-time="<?php echo $zoomMaxTime ?>"></div>
<?php
}
?>