From eb7fe19b017bf1c23c052438391d7fea1b13dfed Mon Sep 17 00:00:00 2001 From: Leigh B Stoller <stoller@flux.utah.edu> Date: Wed, 29 Jan 2014 12:32:00 -0700 Subject: [PATCH] First cut a new page to create new profiles, plus backend scripts and libraries. Rough, needs plenty more work. --- apt/APT_Profile.pm.in | 223 ++++++++++++++++++++++++++++ apt/GNUmakefile.in | 92 ++++++++++++ apt/manage_profile.in | 271 +++++++++++++++++++++++++++++++++++ sql/database-create.sql | 22 +++ sql/database-fill.sql | 6 + sql/updates/4/379 | 59 ++++++++ www/aptui/js/main.js | 30 ++-- www/aptui/js/quickvm_sup.js | 69 +++++---- www/aptui/login.php | 2 +- www/aptui/logout.php | 56 ++++++++ www/aptui/manage_profile.php | 235 +++++++++++++++++++++++++++--- www/aptui/quickvm.css | 35 ++++- www/aptui/quickvm.php | 18 ++- www/aptui/quickvm_sup.php | 45 +++--- 14 files changed, 1068 insertions(+), 95 deletions(-) create mode 100644 apt/APT_Profile.pm.in create mode 100644 apt/GNUmakefile.in create mode 100644 apt/manage_profile.in create mode 100644 sql/updates/4/379 create mode 100644 www/aptui/logout.php diff --git a/apt/APT_Profile.pm.in b/apt/APT_Profile.pm.in new file mode 100644 index 0000000000..569b07ecc5 --- /dev/null +++ b/apt/APT_Profile.pm.in @@ -0,0 +1,223 @@ +#!/usr/bin/perl -wT +# +# Copyright (c) 2007-2014 University of Utah and the Flux Group. +# +# {{{EMULAB-LICENSE +# +# This file is part of the Emulab network testbed software. +# +# This file is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# This file is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public +# License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this file. If not, see <http://www.gnu.org/licenses/>. +# +# }}} +# +package APT_Profile; + +use strict; +use Carp; +use Exporter; +use vars qw(@ISA @EXPORT $AUTOLOAD); + +@ISA = "Exporter"; +@EXPORT = qw ( ); + +# Must come after package declaration! +use EmulabConstants; +use emdb; +use libtestbed; +use English; +use Data::Dumper; +use overload ('""' => 'Stringify'); + +# Configure variables +my $TB = "@prefix@"; +my $TBOPS = "@TBOPSEMAIL@"; + +# Cache of instances to avoid regenerating them. +my %profiles = (); +my $debug = 0; + +# +# Lookup by idx or pid,name or uuid, depending on the args. +# +sub Lookup($$;$) +{ + my ($class, $arg1, $arg2) = @_; + my $idx; + + # + # A single arg is either an index or a "pid,name" or "pid/name" string. + # + if (!defined($arg2)) { + if ($arg1 =~ /^(\d*)$/) { + $idx = $1; + } + elsif ($arg1 =~ /^([-\w]*),([-\w\.\+]*)$/ || + $arg1 =~ /^([-\w]*)\/([-\w\.\+]*)$/) { + $arg1 = $1; + $arg2 = $2; + } + elsif ($arg1 =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/) { + my $result = + DBQueryWarn("select idx from apt_profiles ". + "where uuid='$arg1'"); + + return undef + if (! $result || !$result->numrows); + + ($idx) = $result->fetchrow_array(); + } + else { + return undef; + } + } + elsif (! (($arg1 =~ /^[-\w\.\+]*$/) && ($arg2 =~ /^[-\w\.\+]*$/))) { + return undef; + } + + # + # Two args means pid/name lookup instead of idx. + # + if (defined($arg2)) { + my $result = + DBQueryWarn("select idx from apt_profiles ". + "where pid='$arg1' and name='$arg2'"); + + return undef + if (! $result || !$result->numrows); + + ($idx) = $result->fetchrow_array(); + } + + # Look in cache first + return $profiles{"$idx"} + if (exists($profiles{"$idx"})); + + my $query_result = + DBQueryWarn("select * from apt_profiles where idx='$idx'"); + + return undef + if (!$query_result || !$query_result->numrows); + + my $self = {}; + $self->{'PROFILE'} = $query_result->fetchrow_hashref(); + + bless($self, $class); + + # Add to cache. + $profiles{"$idx"} = $self; + + return $self; +} + +AUTOLOAD { + my $self = $_[0]; + my $type = ref($self) or croak "$self is not an object"; + my $name = $AUTOLOAD; + $name =~ s/.*://; # strip fully-qualified portion + + # A DB row proxy method call. + if (exists($self->{'PROFILE'}->{$name})) { + return $self->{'PROFILE'}->{$name}; + } + carp("No such slot '$name' field in class $type"); + return undef; +} + +# Break circular reference someplace to avoid exit errors. +sub DESTROY { + my $self = shift; + + $self->{'PROFILE'} = undef; +} + +# +# Refresh a class instance by reloading from the DB. +# +sub Refresh($) +{ + my ($self) = @_; + + return -1 + if (! ref($self)); + + my $idx = $self->idx(); + + my $query_result = + DBQueryWarn("select * from apt_profiles where idx=$idx"); + + return -1 + if (!$query_result || !$query_result->numrows); + + $self->{'PROFILE'} = $query_result->fetchrow_hashref(); + + return 0; +} + +# +# Create a profile +# +sub Create($$$$$) +{ + my ($class, $project, $creator, $argref, $usrerr_ref) = @_; + + my $name = DBQuoteSpecial($argref->{'name'}); + my $pid = $project->pid(); + my $pid_idx = $project->pid_idx(); + my $uid = $creator->uid(); + my $uid_idx = $creator->uid_idx(); + + # + # The pid/imageid has to be unique, so lock the table for the check/insert. + # + DBQueryWarn("lock tables apt_profiles write") + or return undef; + + my $query_result = + DBQueryWarn("select name from apt_profiles ". + "where pid_idx='$pid_idx' and name=$name"); + + if ($query_result->numrows) { + DBQueryWarn("unlock tables"); + $$usrerr_ref = "Profile already exists in project!"; + return undef; + } + + my $uuid = NewUUID(); + my $desc = DBQuoteSpecial($argref->{'description'}); + my $rspec = DBQuoteSpecial($argref->{'rspec'}); + + + my $query = "insert into apt_profiles set created=now()"; + + # Append the rest + $query .= ",name=$name"; + $query .= ",uuid='$uuid'"; + $query .= ",pid='$pid',pid_idx='$pid_idx'"; + $query .= ",creator='$uid',creator_idx='$uid_idx'"; + $query .= ",description=$desc"; + $query .= ",rspec=$rspec"; + $query .= ",public=1" + if (exists($argref->{'public'}) && $argref->{'public'}); + + if (! DBQueryWarn($query)) { + DBQueryWarn("unlock tables"); + tberror("Error inserting new apt_profile record for $pid/$name!"); + return undef; + } + DBQueryWarn("unlock tables"); + return Lookup($class, $pid, $argref->{'name'}); +} + +# _Always_ make sure that this 1 is at the end of the file... +1; diff --git a/apt/GNUmakefile.in b/apt/GNUmakefile.in new file mode 100644 index 0000000000..4721a338b0 --- /dev/null +++ b/apt/GNUmakefile.in @@ -0,0 +1,92 @@ +# +# Copyright (c) 2000-2014 University of Utah and the Flux Group. +# +# {{{EMULAB-LICENSE +# +# This file is part of the Emulab network testbed software. +# +# This file is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# This file is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public +# License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this file. If not, see <http://www.gnu.org/licenses/>. +# +# }}} +# + +SRCDIR = @srcdir@ +TESTBED_SRCDIR = @top_srcdir@ +OBJDIR = .. +SUBDIR = apt + +include $(OBJDIR)/Makeconf + +SUBDIRS = + +BIN_SCRIPTS = manage_profile +SBIN_SCRIPTS = +LIB_SCRIPTS = APT_Profile.pm +WEB_BIN_SCRIPTS = webmanage_profile +WEB_SBIN_SCRIPTS= +LIBEXEC_SCRIPTS = $(WEB_BIN_SCRIPTS) $(WEB_SBIN_SCRIPTS) + +# These scripts installed setuid, with sudo. +SETUID_BIN_SCRIPTS = +SETUID_SBIN_SCRIPTS = +SETUID_SUEXEC_SCRIPTS= + +# +# Force dependencies on the scripts so that they will be rerun through +# configure if the .in file is changed. +# +all: $(BIN_SCRIPTS) $(SBIN_SCRIPTS) $(LIBEXEC_SCRIPTS) $(SUBDIRS) \ + $(LIB_SCRIPTS) all-subdirs + +subboss: + +include $(TESTBED_SRCDIR)/GNUmakerules + +install: $(addprefix $(INSTALL_BINDIR)/, $(BIN_SCRIPTS)) \ + $(addprefix $(INSTALL_SBINDIR)/, $(SBIN_SCRIPTS)) \ + $(addprefix $(INSTALL_LIBDIR)/, $(LIB_SCRIPTS)) \ + $(addprefix $(INSTALL_LIBEXECDIR)/, $(LIBEXEC_SCRIPTS)) \ + +boss-install: install install-subdirs + +subboss-install: + +post-install: + +# +# Control node installation (aka, ops) +# +control-install: + +# This rule says what web* script depends on which installed binary directory. +$(WEB_SBIN_SCRIPTS): $(INSTALL_SBINDIR) +$(WEB_BIN_SCRIPTS): $(INSTALL_BINDIR) + +# Just in case the dirs are not yet created, +$(INSTALL_SBINDIR) $(INSTALL_BINDIR): + +# And then how to turn the template into the actual script. +$(WEB_SBIN_SCRIPTS) $(WEB_BIN_SCRIPTS): $(TESTBED_SRCDIR)/WEBtemplate.in + @echo "Generating $@" + cat $< | sed -e 's,@PROGTOINVOKE@,$(word 2,$^)/$(subst web,,$@),' > $@ + +clean: clean-subdirs + +# How to recursively descend into subdirectories to make general +# targets such as `all'. +%.MAKE: + @$(MAKE) -C $(dir $@) $(basename $(notdir $@)) +%-subdirs: $(addsuffix /%.MAKE,$(SUBDIRS)) ; + +.PHONY: $(SUBDIRS) install diff --git a/apt/manage_profile.in b/apt/manage_profile.in new file mode 100644 index 0000000000..5cb0984eb0 --- /dev/null +++ b/apt/manage_profile.in @@ -0,0 +1,271 @@ +#!/usr/bin/perl -w +# +# Copyright (c) 2000-2014 University of Utah and the Flux Group. +# +# {{{EMULAB-LICENSE +# +# This file is part of the Emulab network testbed software. +# +# This file is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# This file is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public +# License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this file. If not, see <http://www.gnu.org/licenses/>. +# +# }}} +# +use English; +use strict; +use Getopt::Std; +use XML::Simple; +use Data::Dumper; +use CGI; + +# +# Back-end script to manage APT profiles. +# +sub usage() +{ + print("Usage: manage_profile [-v] <xmlfile>\n"); + print("Usage: manage_profile -r profile\n"); + exit(-1); +} +my $optlist = "dv"; +my $debug = 0; +my $verify = 0; # Check data and return status only. +my $skipadmin = 0; + +# +# Configure variables +# +my $TB = "@prefix@"; +my $TBOPS = "@TBOPSEMAIL@"; + +# +# Untaint the path +# +$ENV{'PATH'} = "$TB/bin:$TB/sbin:/bin:/usr/bin:/usr/bin:/usr/sbin"; +delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + +# +# Turn off line buffering on output +# +$| = 1; + +# +# Load the Testbed support stuff. +# +use lib "@prefix@/lib"; +use EmulabConstants; +use emdb; +use emutil; +use User; +use Project; +use APT_Profile; + +# Protos +sub fatal($); +sub UserError(); + +# +# Parse command arguments. Once we return from getopts, all that should be +# left are the required arguments. +# +my %options = (); +if (! getopts($optlist, \%options)) { + usage(); +} +if (defined($options{"d"})) { + $debug = 1; +} +if (defined($options{"v"})) { + $verify = 1; +} +if (@ARGV != 1) { + usage(); +} +# The web interface (and in the future the xmlrpc interface) sets this. +my $this_user = User->ImpliedUser(); +if (! defined($this_user)) { + $this_user = User->ThisUser(); + if (!defined($this_user)) { + fatal("You ($UID) do not exist!"); + } +} + +# Remove profile. +if (defined($options{"r"})) { + exit(DeleteProfile()); +} +my $xmlfile = shift(@ARGV); + +# +# These are the fields that we allow to come in from the XMLfile. +# +my $SLOT_OPTIONAL = 0x1; # The field is not required. +my $SLOT_REQUIRED = 0x2; # The field is required and must be non-null. +my $SLOT_ADMINONLY = 0x4; # Only admins can set this field. +# +# XXX We should encode all of this in the DB so that we can generate the +# forms on the fly, as well as this checking code. +# +my %xmlfields = + # XML Field Name DB slot name Flags Default + ("profile_name" => ["name", $SLOT_REQUIRED], + "profile_pid" => ["pid", $SLOT_REQUIRED], + "profile_creator" => ["creator", $SLOT_OPTIONAL], + "profile_description" => ["description", $SLOT_REQUIRED], + "profile_public" => ["public", $SLOT_OPTIONAL], + "rspec" => ["rspec", $SLOT_REQUIRED], +); + +# +# Must wrap the parser in eval since it exits on error. +# +my $xmlparse = eval { XMLin($xmlfile, + VarAttr => 'name', + ContentKey => '-content', + SuppressEmpty => undef); }; +fatal($@) + if ($@); + +# +# Process and dump the errors (formatted for the web interface). +# We should probably XML format the errors instead but not sure I want +# to go there yet. +# +my %errors = (); + +# +# Make sure all the required arguments were provided. +# +my $key; +foreach $key (keys(%xmlfields)) { + my (undef, $required, undef) = @{$xmlfields{$key}}; + + $errors{$key} = "Required value not provided" + if ($required & $SLOT_REQUIRED && + ! exists($xmlparse->{'attribute'}->{"$key"})); +} +UserError() + if (keys(%errors)); + +# +# We build up an array of arguments to create. +# +my %new_args = (); + +foreach $key (keys(%{ $xmlparse->{'attribute'} })) { + my $value = $xmlparse->{'attribute'}->{"$key"}->{'value'}; + if (!defined($value)) { # Empty string comes from XML as an undef value. + $xmlparse->{'attribute'}->{"$key"}->{'value'} = $value = ""; + } + + print STDERR "User attribute: '$key' -> '$value'\n" + if ($debug); + + my $field = $key; + if (!exists($xmlfields{$field})) { + next; # Skip it. + } + my ($dbslot, $required, $default) = @{$xmlfields{$field}}; + + if ($required & $SLOT_REQUIRED) { + # A slot that must be provided, so do not allow a null value. + if (!defined($value)) { + $errors{$key} = "Must provide a non-null value"; + next; + } + } + if ($required & $SLOT_OPTIONAL) { + # Optional slot. If value is null skip it. Might not be the correct + # thing to do all the time? + if (!defined($value)) { + next + if (!defined($default)); + $value = $default; + } + } + if ($required & $SLOT_ADMINONLY && !$skipadmin) { + # Admin implies optional, but thats probably not correct approach. + $errors{$key} = "Administrators only" + if (! $this_user->IsAdmin()); + } + + # Now check that the value is legal. + if (! TBcheck_dbslot($value, "apt_profiles", + $dbslot, TBDB_CHECKDBSLOT_ERROR)) { + $errors{$key} = TBFieldErrorString(); + next; + } + $new_args{$dbslot} = $value; +} +UserError() + if (keys(%errors)); + +# +# We need to make sure the project exists and is a valid project for +# the creator (current user). +# +my $project = Project->Lookup($new_args{"pid"}); +if (!defined($project)) { + $errors{"profile_pid"} = "No such project exists"; +} +elsif (!$project->AccessCheck($this_user, TB_PROJECT_MAKEIMAGEID())) { + $errors{"profile_pid"} = "Not enough permission in this project"; +} + +my $usererror; +my $profile = APT_Profile->Create($project, $this_user, \%new_args, \$usererror); +if (!defined($profile)) { + if (defined($usererror)) { + $errors{"profile_name"} = $usererror; + UserError(); + } + fatal("Could not create new profile"); +} +exit(0); + +sub fatal($) +{ + my ($mesg) = @_; + + print STDERR "*** $0:\n". + " $mesg\n"; + # Exit with negative status so web interface treats it as system error. + exit(-1); +} + +# +# Generate a simple XML file that PHP can parse. The web interface +# relies on using the same name attributes for the errors, as for +# the incoming values. +# +sub UserError() +{ + if (keys(%errors)) { + print "<errors>\n"; + foreach my $key (keys(%errors)) { + print "<error name='$key'>" . CGI::escapeHTML($errors{$key}); + print "</error>\n"; + } + print "</errors>\n"; + } + # Exit with positive status so web interface treats it as user error. + exit(1); +} + +sub escapeshellarg($) +{ + my ($str) = @_; + + $str =~ s/[^[:alnum:]]/\\$&/g; + return $str; +} diff --git a/sql/database-create.sql b/sql/database-create.sql index 2a232bde6e..a8f036653d 100644 --- a/sql/database-create.sql +++ b/sql/database-create.sql @@ -54,6 +54,28 @@ CREATE TABLE `active_checkups` ( PRIMARY KEY (`object`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1; +-- +-- Table structure for table `apt_profiles` +-- + +DROP TABLE IF EXISTS `apt_profiles`; +CREATE TABLE `apt_profiles` ( + `name` varchar(64) NOT NULL default '', + `idx` int(10) unsigned NOT NULL auto_increment, + `creator` varchar(8) NOT NULL default '', + `creator_idx` mediumint(8) unsigned NOT NULL default '0', + `pid` varchar(48) NOT NULL default '', + `pid_idx` mediumint(8) unsigned NOT NULL default '0', + `created` datetime default NULL, + `uuid` varchar(40) NOT NULL, + `public` tinyint(1) NOT NULL default '0', + `description` mediumtext, + `rspec` mediumtext, + PRIMARY KEY (`idx`), + UNIQUE KEY `pidname` (`pid_idx`,`name`), + UNIQUE KEY `uuid` (`uuid`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1; + -- -- Table structure for table `archive_revisions` -- diff --git a/sql/database-fill.sql b/sql/database-fill.sql index 0dbda77688..0b532cb1ca 100644 --- a/sql/database-fill.sql +++ b/sql/database-fill.sql @@ -1269,6 +1269,12 @@ REPLACE INTO table_regex VALUES ('default','tinytext_utf8','text','regex','^(?:[ REPLACE INTO table_regex VALUES ('default','text_utf8','text','regex','^(?:[\\x20-\\x7E]|[\\xC2-\\xDF][\\x80-\\xBF]|\\xE0[\\xA0-\\xBF][\\x80-\\xBF]|[\\xE1-\\xEC\\xEE\\xEF][\\x80-\\xBF]{2}|\\xED[\\x80-\\x9F][\\x80-\\xBF])*$',0,65535,'adopted from http://www.w3.org/International/questions/qa-forms-utf-8.en.php'); REPLACE INTO table_regex VALUES ('default','fulltext_utf8','text','regex','^(?:[\\x09\\x0A\\x0D\\x20-\\x7E]|[\\xC2-\\xDF][\\x80-\\xBF]|\\xE0[\\xA0-\\xBF][\\x80-\\xBF]|[\\xE1-\\xEC\\xEE\\xEF][\\x80-\\xBF]{2}|\\xED[\\x80-\\x9F][\\x80-\\xBF])*$',0,65535,'adopted from http://www.w3.org/International/questions/qa-forms-utf-8.en.php'); +REPLACE INTO table_regex VALUES ('apt_profiles','pid','text','redirect','projects:pid',0,0,NULL); +REPLACE INTO table_regex VALUES ('apt_profiles','creator','text','redirect','users:uid',0,0,NULL); +REPLACE INTO table_regex VALUES ('apt_profiles','name','text','redirect','images:imagename',0,64,NULL); +REPLACE INTO table_regex VALUES ('apt_profiles','public','int','redirect','default:boolean',0,0,NULL); +REPLACE INTO table_regex VALUES ('apt_profiles','description','text','redirect','default:html_fulltext',0,512,NULL); +REPLACE INTO table_regex VALUES ('apt_profiles','rspec','text','redirect','default:html_fulltext',0,8192,NULL); -- -- Dumping data for table `testsuite_preentables` diff --git a/sql/updates/4/379 b/sql/updates/4/379 new file mode 100644 index 0000000000..20d2151836 --- /dev/null +++ b/sql/updates/4/379 @@ -0,0 +1,59 @@ +# +# Add APT profiles table. +# +use strict; +use libdb; + +sub DoUpdate($$$) +{ + my ($dbhandle, $dbname, $version) = @_; + + if(!DBTableExists("apt_profiles")) { + DBQueryFatal("CREATE TABLE `apt_profiles` ( ". + " `name` varchar(64) NOT NULL default '', ". + " `idx` int(10) unsigned NOT NULL auto_increment, ". + " `creator` varchar(8) NOT NULL default '', ". + " `creator_idx` mediumint(8) unsigned NOT NULL default '0', ". + " `pid` varchar(48) NOT NULL default '', ". + " `pid_idx` mediumint(8) unsigned NOT NULL default '0', ". + " `created` datetime default NULL, ". + " `uuid` varchar(40) NOT NULL, ". + " `public` tinyint(1) NOT NULL default '0', ". + " `description` mediumtext, ". + " `rspec` mediumtext, ". + " PRIMARY KEY (`idx`), ". + " UNIQUE KEY `pidname` (`pid_idx`,`name`), ". + " UNIQUE KEY `uuid` (`uuid`) ". + ") ENGINE=MyISAM DEFAULT CHARSET=latin1"); + } + + DBQueryFatal("REPLACE INTO table_regex VALUES ". + "('apt_profiles','pid','text','redirect',". + "'projects:pid',0,0,NULL)"); + + DBQueryFatal("REPLACE INTO table_regex VALUES ". + "('apt_profiles','creator','text','redirect',". + "'users:uid',0,0,NULL)"); + + DBQueryFatal("REPLACE INTO table_regex VALUES ". + "('apt_profiles','name','text','redirect',". + "'images:imagename',0,64,NULL)"); + + DBQueryFatal("REPLACE INTO table_regex VALUES ". + "('apt_profiles','public','int','redirect',". + "'default:boolean',0,0,NULL)"); + + DBQueryFatal("REPLACE INTO table_regex VALUES ". + "('apt_profiles','description','text','redirect',". + "'default:html_fulltext',0,512,NULL)"); + + DBQueryFatal("REPLACE INTO table_regex VALUES ". + "('apt_profiles','rspec','text','redirect',". + "'default:html_fulltext',0,8192,NULL)"); + + return 0; +} + +# Local Variables: +# mode:perl +# End: diff --git a/www/aptui/js/main.js b/www/aptui/js/main.js index 6ce07b17da..d526b0d6a6 100644 --- a/www/aptui/js/main.js +++ b/www/aptui/js/main.js @@ -68,19 +68,21 @@ function ($, sup) } } else if (pageType == 'manage_profile') { - try { - $('#rspecfile').change(function() { - var reader = new FileReader(); - reader.onload = function(event) { - var content = event.target.result; + if (0) { + try { + $('#rspecfile').change(function() { + var reader = new FileReader(); + reader.onload = function(event) { + var content = event.target.result; - sup.ShowUploadedRspec(content); - }; - reader.readAsText(this.files[0]); - }); - } - catch (e) { - alert(e); + sup.ShowUploadedRspec(content); + }; + reader.readAsText(this.files[0]); + }); + } + catch (e) { + alert(e); + } } } $('body').show(); @@ -144,6 +146,10 @@ function ($, sup) event.preventDefault(); sup.LoginByModal(); }); + $('#logout_button').click(function (event) { + event.preventDefault(); + sup.Logout(); + }); } $(document).ready(initialize); diff --git a/www/aptui/js/quickvm_sup.js b/www/aptui/js/quickvm_sup.js index 740485b4df..f28d2afb55 100755 --- a/www/aptui/js/quickvm_sup.js +++ b/www/aptui/js/quickvm_sup.js @@ -142,7 +142,8 @@ function ShowTopo(uuid) function UpdateProfileSelection(selectedElement) { - var profile = $(selectedElement).attr('value'); + console.log(selectedElement); + var profile = $(selectedElement).text(); $('#selected_profile').attr('value', profile); $('#selected_profile_text').html("" + profile); @@ -178,15 +179,15 @@ function ShowProfileList(selectedElement) var topo = ConvertManifestToJSON(profile, xml); console.info(topo); - $('#showtopo_title').html("<h3>" + profile + "</h3>"); + $('#showtopo_title').html("<h3>" + json.value.name + "</h3>"); $('#showtopo_description').html(json.value.description); maketopmap("#showtopo_div", ($("#showtopo_div").outerWidth()), 300, topo); - } - var $xmlthing = CallMethod("getprofile", null, 0, profile); - $xmlthing.done(callback); + } + var $xmlthing = CallMethod("getprofile", null, 0, profile); + $xmlthing.done(callback); } function ShowProfile(direction) @@ -236,29 +237,6 @@ function ShowProfile(direction) $xmlthing.done(callback); } -function ShowProfileSlider(direction) -{ - console.info(direction); - - var callback = function(json) { - console.info(json.value); - var xmlDoc = $.parseXML(json.value.rspec); - var xml = $(xmlDoc); - var topo = ConvertManifestToJSON(null, xml); - console.info(topo); - - $('#showtopo_title').html("<h3>" + json.value.name + "</h3>"); - $('#showtopo_description').html(json.value.description); - - $("#slider_container").removeClass("invisible"); - maketopmap("#slider_div", - ($("#slider_div").outerWidth()) - 90, - 200, topo); - } - var $xmlthing = CallMethod("getprofile", null, 0, null); - $xmlthing.done(callback); -} - function InitProfileSelector() { $('#scrollleft').click(function () { @@ -976,6 +954,38 @@ function LoginByModal() xmlthing.done(callback); } +/* + * log the user out via an ajax call. + */ +function Logout() +{ + var callback = function(json) { + if (json.code) { + alert("Logout failed!"); + } + else { + // Need to stick the button back in ... + $("#loginbutton").html(""); + } + } + var xmlthing = $.ajax({ + // the URL for the request + url: "logout.php", + + // the data to send (will be converted to a query string) + data: { + ajax_request: 1, + }, + + // whether this is a POST or GET request + type: "GET", + + // the type of data we expect back + dataType : "json", + }); + xmlthing.done(callback); +} + // Exports from this module for use elsewhere return { Extend: Extend, @@ -990,6 +1000,7 @@ return { Terminate: Terminate, UpdateProfileSelection: UpdateProfileSelection, ShowUploadedRspec: ShowUploadedRspec, - LoginByModal: LoginByModal + LoginByModal: LoginByModal, + Logout: Logout }; }); diff --git a/www/aptui/login.php b/www/aptui/login.php index ba8e44401c..bc702b9941 100644 --- a/www/aptui/login.php +++ b/www/aptui/login.php @@ -203,6 +203,6 @@ else { # # Zap back to front page in secure mode. # - header("Location: $APTBASE"); + header("Location: $APTBASE/quickvm.php"); } ?> diff --git a/www/aptui/logout.php b/www/aptui/logout.php new file mode 100644 index 0000000000..ecb58ef5f8 --- /dev/null +++ b/www/aptui/logout.php @@ -0,0 +1,56 @@ +<?php +# +# Copyright (c) 2000-2014 University of Utah and the Flux Group. +# +# {{{EMULAB-LICENSE +# +# This file is part of the Emulab network testbed software. +# +# This file is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# This file is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public +# License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this file. If not, see <http://www.gnu.org/licenses/>. +# +# }}} +# +chdir(".."); +include("defs.php3"); +chdir("apt"); +include("quickvm_sup.php"); + +# +# Verify page arguments. +# +$optargs = OptionalPageArguments("ajax_request", PAGEARG_BOOLEAN); + +# +# Get current user. +# +$this_user = CheckLogin($check_status); +if ($this_user) { + if (DOLOGOUT($this_user) != 0) { + if ($ajax_request) { + SPITAJAX_ERROR(1, "Logout failed"); + exit(); + } + else { + SPITHEADER(); + echo "<center><font color=red>Logout failed!</font></failed>\n"; + SPITFOOTER(); + } + } +} +if ($ajax_request) { + SPITAJAX_RESPONSE(""); + exit(); +} +header("Location: quickvm.php"); +?> diff --git a/www/aptui/manage_profile.php b/www/aptui/manage_profile.php index 645fda2684..0113e6395f 100644 --- a/www/aptui/manage_profile.php +++ b/www/aptui/manage_profile.php @@ -35,15 +35,14 @@ $this_user = CheckLogin($check_status); # Verify page arguments. # $optargs = OptionalPageArguments("create", PAGEARG_STRING, - "profile", PAGEARG_STRING, "formfields", PAGEARG_ARRAY); # # Spit the form # -function SPITFORM($formfields, $needlogin, $errors) +function SPITFORM($formfields, $errors) { - global $this_user; + global $this_user, $projlist; # XSS prevention. while (list ($key, $val) = each ($formfields)) { @@ -56,15 +55,20 @@ function SPITFORM($formfields, $needlogin, $errors) } } - $formatter = function($field, $label, $html) use ($errors) { + $formatter = function($field, $label, $html, $help = null) use ($errors) { $class = "form-group"; if ($errors && array_key_exists($field, $errors)) { $class .= " has-error"; } echo "<div class='$class'>\n"; echo " <label for='$field' ". - " class='col-sm-2 control-label'>$label</label>\n"; - echo " <div class='col-sm-10'>\n"; + " class='col-sm-3 control-label'>$label "; + if ($help) { + echo "<a href='#' data-toggle='tooltip' title='$help'>". + "<span class='glyphicon glyphicon-question-sign'></span></a>"; + } + echo " </label>\n"; + echo " <div class='col-sm-7'>\n"; echo " $html\n"; if ($errors && array_key_exists($field, $errors)) { echo "<label class='control-label' for='inputError'>" . @@ -88,6 +92,7 @@ function SPITFORM($formfields, $needlogin, $errors) </h3> </div> <div class='panel-body'>\n"; + echo " <form id='quickvm_create_profile_form' class='form-horizontal' role='form' enctype='multipart/form-data' @@ -96,13 +101,45 @@ function SPITFORM($formfields, $needlogin, $errors) echo " <div class='col-sm-12'>\n"; echo " <fieldset>\n"; + # + # Look for non-specific error. + # + if ($errors && array_key_exists("error", $errors)) { + echo "<font color=red>" . $errors["error"] . "</font>"; + } $formatter("profile_name", "Profile Name", "<input name=\"formfields[profile_name]\" id='profile_name' value='" . $formfields["profile_name"] . "' class='form-control' - placeholder='' type='text'>"); + placeholder='' type='text'>", + "alphanumeric, dash, underscore, no whitespace"); + # + # If user is a member of only one project, then just pass it + # through, no need for the user to see it. Otherwise we have + # to let the user choose. + # + if (count($projlist) == 1) { + echo "<input type='hidden' name='formfields[profile_pid]' ". + "value='" . $projlist[0] . "'>\n"; + } + else { + $pid_options = ""; + while (list($project) = each($projlist)) { + $selected = ""; + if ($formfields["profile_pid"] == $project) { + $selected = "selected"; + } + $pid_options .= + "<option $selected value='$project'>$project</option>\n"; + } + $formatter("profile_pid", "Project", + "<select name=\"formfields[profile_pid]\" + id='profile_pid' class='form-control' + placeholder='Please Select'>$pid_options</select>"); + } + $formatter("profile_description", "Description", "<textarea name=\"formfields[profile_description]\" id='profile_description' @@ -115,16 +152,24 @@ function SPITFORM($formfields, $needlogin, $errors) "<input name='rspecfile' id='rspecfile' type=file class='form-control'>"); + $formatter("profile_public", "Public?", + "<div class='checkbox'> + <label><input name=\"formfields[profile_public]\" ". + $formfields["profile_public"] . + " id='profile_public' value=checked + type=checkbox> ". + "List on the public page for anyone to use?</label></div>"); + echo " </fieldset>\n"; echo "<div class='form-group'> <div class='col-sm-offset-2 col-sm-10'> - <button class='btn btn-primary btm-sm' + <button class='btn btn-primary btm-sm pull-right' id='profile_submit_button' type='submit' name='create'>Create</button> </div> </div>\n"; - + echo " </div>\n"; echo " </div>\n"; echo " </form></div>\n"; @@ -163,38 +208,182 @@ function SPITFORM($formfields, $needlogin, $errors) SPITFOOTER(); } + # -# If not clicked, then put up a form. We use a session variable to -# save the form data in case we have to force the user to login. +# The user must be logged in. # -session_start(); +if (!$this_user) { + if (isset($formfields)) { + $_SESSION["formfields"] = $formfields; + } + # HTTP_REFERER will not work reliably when redirecting so + # pass in the URI for this page as an argument + header("Location: login.php?referrer=". + urlencode($_SERVER['REQUEST_URI'])); + # Use exit because of the session. + exit(); +} + +# +# See what projects the user can do this in. +# +$projlist = $this_user->ProjectAccessList($TB_PROJECT_MAKEIMAGEID); if (! isset($create)) { + $errors = array(); + if (isset($_SESSION["formfields"])) { $defaults = $_SESSION["formfields"]; } else { $defaults = array(); } + if (! (isset($projlist) && count($projlist))) { + $errors["error"] = + "You do not appear to be a member of any projects in which ". + "you have permission to create new profiles"; + } + SPITFORM($defaults, $errors); + return; +} + +# +# Otherwise, must validate and redisplay if errors +# +$errors = array(); + +# +# Quick check for required fields. +# +$required = array("pid", "name", "description"); + +foreach ($required as $key) { + if (!isset($formfields["profile_${key}"]) || + strcmp($formfields["profile_${key}"], "") == 0) { + $errors["profile_${key}"] = "Missing Field"; + } + elseif (! TBcheck_dbslot($formfields["profile_${key}"], "apt_profiles", $key, + TBDB_CHECKDBSLOT_WARN|TBDB_CHECKDBSLOT_ERROR)) { + $errors["profile_${key}"] = TBFieldErrorString(); + } +} + +# +# The rspec has to be treated specially of course. +# +if (isset($_FILES['rspecfile']) && + $_FILES['rspecfile']['name'] != "" && + $_FILES['rspecfile']['name'] != "none") { + + $rspec = file_get_contents($_FILES['rspecfile']['tmp_name']); + if (!$rspec) { + $errors["rspecfile"] = "Could not process file"; + } + elseif (! TBvalid_html_fulltext($rspec)) { + $errors["rspecfile"] = TBFieldErrorString(); + } +} +else { + $errors["rspecfile"] = "Missing Field"; +} + +# +# Project has to exist. We need to know it for the SUEXEC call +# below. +# +$project = Project::LookupByPid($formfields["profile_pid"]); +if (!$project) { + $errors["profile_pid"] = "No such project"; +} - SPITFORM($defaults, 0, null); +# Present these errors before we call out to do anything else. +if (count($errors)) { + SPITFORM($formfields, $errors); return; } # -# The user must be logged in. +# Pass to the backend as an XML data file. If this gets too complicated, +# we might eed to do all the checking in the backend and have it pass +# back the error set. # -if (!$this_user) { - if (isset($formfields)) { - $_SESSION["formfields"] = $formfields; +# Generate a temporary file and write in the XML goo. +# +$xmlname = tempnam("/tmp", "newprofile"); +if (! $xmlname) { + TBERROR("Could not create temporary filename", 0); + $errors["error"] = "Internal error; Could not create temp file"; +} +elseif (! ($fp = fopen($xmlname, "w"))) { + TBERROR("Could not open temp file $xmlname", 0); + $errors["error"] = "Internal error; Could not open temp file"; +} +else { + fwrite($fp, "<profile>\n"); + fwrite($fp, "<attribute name='profile_pid'>"); + fwrite($fp, " <value>" . $formfields["profile_pid"] . "</value>"); + fwrite($fp, "</attribute>\n"); + fwrite($fp, "<attribute name='profile_name'>"); + fwrite($fp, " <value>" . + htmlspecialchars($formfields["profile_name"]) . "</value>"); + fwrite($fp, "</attribute>\n"); + fwrite($fp, "<attribute name='profile_description'>"); + fwrite($fp, " <value>" . + htmlspecialchars($formfields["profile_description"]) . "</value>"); + fwrite($fp, "</attribute>\n"); + fwrite($fp, "<attribute name='rspec'>"); + fwrite($fp, " <value>" . htmlspecialchars($rspec) . "</value>"); + fwrite($fp, "</attribute>\n"); + if (isset($formfields["profile_public"]) && + $formfields["profile_public"] == "checked") { + fwrite($fp, "<attribute name='profile_public'>"); + fwrite($fp, " <value>1</value>"); + fwrite($fp, "</attribute>\n"); } - header("Location: login.php?refer=1"); - # Use exit because of the session. - exit(); + fwrite($fp, "</profile>\n"); + fclose($fp); + chmod($xmlname, 0666); +} +if (count($errors)) { + unlink($xmlname); + SPITFORM($formfields, $errors); + return; } -# No longer needed, the user is logged in. -session_destroy(); -SPITFORM($formfields, 0, null); +# +# Call out to the backend. +# +$retval = SUEXEC($this_user->uid(), $project->unix_gid(), + "webmanage_profile $xmlname", + SUEXEC_ACTION_IGNORE); +if ($retval) { + if ($retval < 0) { + $errors["error"] = "Internal Error; please try again later."; + SUEXECERROR(SUEXEC_ACTION_CONTINUE); + } + else { + # + # Decode simple XML that is returned. + # + $parsed = simplexml_load_string($suexec_output); + if (!$parsed) { + $errors["error"] = "Internal Error; please try again later."; + TBERROR("Could not parse XML output:\n$suexec_output\n", 0); + } + else { + foreach ($parsed->error as $error) { + $errors[(string)$error['name']] = $error; + } + } + } +} +if (count($errors)) { + unlink($xmlname); + SPITFORM($formfields, $errors); + return; +} + +SPITFORM($formfields, $errors); + ?> diff --git a/www/aptui/quickvm.css b/www/aptui/quickvm.css index 028c0a3580..4f95f5bc4f 100644 --- a/www/aptui/quickvm.css +++ b/www/aptui/quickvm.css @@ -17,13 +17,36 @@ body { padding: 0 0 2.5em; } -#loginbutton { - display:inline-block; - padding-right:15px; - padding-top:15px; - position:absolute; - right:0; +.navbar img { + display: block; + margin: 0 auto; + height: 75px; +} + +.navbar-static-top { + background-color: #ff6600; +} + +.navbar-inner { + background-color: #ff6600; + height: 75px; + width: 95%; + margin: 0 auto; +} + +.navbar .brand { + position: absolute; top:0; + left:0; + width: 100%; +} + +.navbar-nav > li > a { + margin-top: 20px; +} + +.navbar-btn { + margin-top: 20px; } /* Set the fixed height of the footer here */ diff --git a/www/aptui/quickvm.php b/www/aptui/quickvm.php index 19ee215e76..a6a0238a90 100755 --- a/www/aptui/quickvm.php +++ b/www/aptui/quickvm.php @@ -54,19 +54,20 @@ $optargs = OptionalPageArguments("create", PAGEARG_STRING, # if (isset($ajax_request)) { if ($ajax_method == "getprofile") { - $profile_name = addslashes($ajax_argument); + $profile_idx = addslashes($ajax_argument); $query_result = - DBQueryWarn("select * from quickvm_rspecs ". - "where name='$profile_name'", $dblink); + DBQueryWarn("select * from apt_profiles ". + "where idx='$profile_idx'"); if (!$query_result || !mysql_num_rows($query_result)) { - SPITAJAX_ERROR(1, "No such profile!"); + SPITAJAX_ERROR(1, "No such profile $profile_idx!"); exit(); } $row = mysql_fetch_array($query_result); SPITAJAX_RESPONSE(array('rspec' => $row['rspec'], 'name' => $row['name'], + 'idx' => $row['idx'], 'description' => $row['description'])); } exit(); @@ -76,13 +77,16 @@ if (isset($ajax_request)) { $username_default = "Pick a user name"; $email_default = "Your email address"; $sshkey_default = "Your SSH public key"; -$profile_default = "UBUNTU12-64-STD"; +$profile_default = "ThreeVMs"; $profile_array = array(); $query_result = - DBQueryFatal("select * from quickvm_rspecs", $dblink); + DBQueryFatal("select * from apt_profiles where public=1"); while ($row = mysql_fetch_array($query_result)) { - $profile_array[$row["name"]] = $row["name"]; + $profile_array[$row["idx"]] = $row["name"]; + if ($row["pid"] == $TBOPSPID && $row["name"] == $profile_default) { + $profile_default = $row["idx"]; + } } function SPITFORM($username, $email, $sshkey, $profile, $newuser, $errors) diff --git a/www/aptui/quickvm_sup.php b/www/aptui/quickvm_sup.php index b9125f4fad..b06ab3f799 100644 --- a/www/aptui/quickvm_sup.php +++ b/www/aptui/quickvm_sup.php @@ -72,32 +72,43 @@ function SPITHEADER($thinheader = 0) if ($TBMAINSITE && file_exists("../google-analytics.php")) { readfile("../google-analytics.php"); } - + echo " <!-- Container for body, needed for sticky footer --> <div id='wrap'> - <div style='background-color: #ff6600'>"; + <div class='navbar navbar-static-top' role='navigation'> + <div class='navbar-inner'> + <div class='brand'> + <img src='aptlogo.png'/> + </div> + <ul class='nav navbar-nav navbar-right'>"; if (!$disable_accounts) { if ($login_user) { - echo "<div id='loginbutton'> - $login_uid logged in<br> - </div>\n"; + echo "<li><a>$login_uid logged in</a></li>\n"; } elseif (!NOLOGINS()) { - echo "<div id='loginbutton'> - <button class='btn btn-primary' - id='login_button' type=button - data-toggle='modal' data-target='#quickvm_login_modal'> - Login</button> - </div>\n"; + echo "<li><a class='btn btn-primary navbar-btn' + id='login_button' + data-toggle='modal' + href='#quickvm_login_modal' + data-target='#quickvm_login_modal'> + Login</a></li> + \n"; } } - echo "<img class='align-center' style='width: ${height}px' - src='aptlogo.png'/> - </div> - <!-- Page content --> - <div class='container'>\n"; + echo " </ul> + <ul class='nav navbar-nav navbar-left'> + <li><a href='quickvm.php'>Home</a></li>\n"; + if (!$disable_accounts && $login_user) { + echo " <li><a href='#' id='logout_button'>Logout</a></li>\n"; + } + echo " </ul> + </div> + </div>\n"; + SpitLoginModal("quickvm_login_modal"); + echo " <!-- Page content --> + <div class='container'>\n"; } function SPITFOOTER() @@ -195,7 +206,7 @@ function SpitVerifyModal($id, $label) function SpitLoginModal($id, $embedded = 0) { echo "<!-- This is the login modal --> - <div id='$id' class='modal fade'> + <div id='$id' class='modal fade' role='dialog'> <div class='modal-dialog'> <div class='modal-content'> <div class='modal-header'> -- GitLab